Skip to content

Commit 4b443a8

Browse files
feat: improved p4 support and config templating (#20)
1 parent be4707b commit 4b443a8

File tree

13 files changed

+427
-62
lines changed

13 files changed

+427
-62
lines changed

deno.jsonc

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"tasks": {
3-
"start": "deno run -A --watch=src src/index.ts",
3+
"dev": "deno run -A --watch=src src/index.ts",
4+
"test": "deno test -A --watch",
45
"run": "deno run -A src/index.ts",
56
"install": "deno install -A --force --global --name runreal src/index.ts",
67
"compile-win": "deno compile -A --target x86_64-pc-windows-msvc --output build/runreal-win-x64 src/index.ts",
@@ -9,15 +10,15 @@
910
"generate-schema": "deno run -A src/generate-schema.ts"
1011
},
1112
"lint": {
12-
"include": ["src/"],
13+
"include": ["src/", "tests/"],
1314
"rules": {
1415
"tags": ["recommended"],
1516
"include": ["ban-untagged-todo"],
1617
"exclude": ["no-unused-vars", "no-explicit-any"]
1718
}
1819
},
1920
"fmt": {
20-
"include": ["src/"],
21+
"include": ["src/", "tests/"],
2122
"useTabs": true,
2223
"lineWidth": 120,
2324
"indentWidth": 2,

src/commands/debug/debug-config.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Command } from '../../deps.ts'
2+
import { config } from '../../lib/config.ts'
3+
import { CliOptions, GlobalOptions } from '../../lib/types.ts'
4+
5+
export type DebugOptions = typeof debugConfig extends Command<any, any, infer Options, any, any> ? Options
6+
: never
7+
8+
export const debugConfig = new Command<GlobalOptions>()
9+
.option('-r, --render', 'Render the config with substitutions')
10+
.description('debug config')
11+
.action((options) => {
12+
const { render } = options as DebugOptions & GlobalOptions
13+
const cfg = config.get(options as CliOptions)
14+
15+
if (render) {
16+
const rendered = config.renderConfig(cfg)
17+
console.dir(rendered, { depth: null })
18+
return
19+
}
20+
21+
console.dir(cfg, { depth: null })
22+
})

src/commands/debug/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Command } from '../../deps.ts'
2+
import { GlobalOptions } from '../../lib/types.ts'
3+
4+
import { debugConfig } from './debug-config.ts'
5+
6+
export const debug = new Command<GlobalOptions>()
7+
.description('debug')
8+
.command('config', debugConfig)

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { VERSION } from './version.ts'
22

3-
import { debug } from './commands/debug.ts'
3+
import { debug } from './commands/debug/index.ts'
44
import { build } from './commands/build.ts'
55
import { engine } from './commands/engine/index.ts'
66
import { init } from './commands/init.ts'

src/lib/config.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import { CliOptions, RunrealConfig } from '../lib/types.ts'
33
import { execSync } from '../lib/utils.ts'
44
import { ConfigSchema, InternalSchema } from '../lib/schema.ts'
55
import { Source } from './source.ts'
6+
import { renderConfig } from './template.ts'
67

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

9-
class Config {
10+
export class Config {
1011
private config: Partial<RunrealConfig> = {
1112
engine: {
1213
path: '',
@@ -19,7 +20,7 @@ class Config {
1920
},
2021
build: {
2122
path: '',
22-
id: '',
23+
id: env('RUNREAL_BUILD_ID') || '',
2324
branch: '',
2425
branchSafe: '',
2526
commit: '',
@@ -32,6 +33,9 @@ class Config {
3233
buildCheckoutPath: env('BUILDKITE_BUILD_CHECKOUT_PATH') || Deno.cwd(),
3334
buildPipelineSlug: env('BUILDKITE_PIPELINE_SLUG') || '',
3435
},
36+
metadata: {
37+
test: env('RUNREAL_BUILD_ID') || '',
38+
},
3539
workflows: [],
3640
}
3741
private cliOptionToConfigMap = {
@@ -66,6 +70,11 @@ class Config {
6670
return this.config as RunrealConfig
6771
}
6872

73+
renderConfig(cfg: RunrealConfig): RunrealConfig {
74+
const rendered = renderConfig(cfg)
75+
return rendered
76+
}
77+
6978
async mergeConfig(configPath: string) {
7079
const cfg = await this.readConfigFile(configPath)
7180
if (!cfg) return

src/lib/schema.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ import { z } from '../deps.ts'
22

33
export const InternalSchema = z.object({
44
buildkite: z.object({
5-
branch: z.string().describe('Buildkite branch name'),
6-
checkout: z.string().describe('Buildkite commit hash'),
7-
buildNumber: z.string().describe('Buildkite build number'),
8-
buildCheckoutPath: z.string().describe('Buildkite build checkout path'),
9-
buildPipelineSlug: z.string().describe('Buildkite pipeline slug'),
10-
}),
5+
branch: z.string().describe('Buildkite branch name').optional(),
6+
checkout: z.string().describe('Buildkite commit hash').optional(),
7+
buildNumber: z.string().describe('Buildkite build number').optional(),
8+
buildCheckoutPath: z.string().describe('Buildkite build checkout path').optional(),
9+
buildPipelineSlug: z.string().describe('Buildkite pipeline slug').optional(),
10+
}).optional(),
11+
metadata: z.object({
12+
test: z.string().describe('Build id <RUNREAL_BUILD_ID>'),
13+
}).optional(),
1114
})
1215

1316
export const ConfigSchema = z.object({
@@ -56,7 +59,6 @@ export const ConfigSchema = z.object({
5659
args: z.array(z.string()).optional().describe('Command arguments'),
5760
}),
5861
),
59-
})
60-
.optional(),
61-
),
62+
}),
63+
).optional(),
6264
})

src/lib/source.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,33 @@ abstract class Base {
2020
abstract clean(): void
2121

2222
safeRef(): string {
23-
return this.ref().replace(/\/\//g, '/').replace(/\//g, '-')
23+
return this.ref().replace(/\/\//g, '/').replace(/\//g, '-').toLowerCase()
2424
}
2525
}
2626

2727
export class Perforce extends Base {
2828
executable: string = 'p4'
29+
stream(): string {
30+
return execSync(this.executable, [
31+
'-F',
32+
'%Stream%',
33+
'-ztag',
34+
'client',
35+
'-o',
36+
], { cwd: this.cwd, quiet: true }).output.trim()
37+
}
38+
changelist(): string {
39+
return execSync(this.executable, [
40+
'-F',
41+
'%change%',
42+
'changes',
43+
'-m1',
44+
], { cwd: this.cwd, quiet: true }).output.trim().replace('Change ', '')
45+
}
2946
ref(): string {
30-
const stream = execSync(this.executable, ['p4', '-F', '"%Stream%"', '-ztag', 'client', '-o'], {
31-
cwd: this.cwd,
32-
quiet: false,
33-
}).output.trim()
34-
const change = execSync(this.executable, ['-F', '%change%', 'changes', '-m1'], { cwd: this.cwd, quiet: false })
35-
.output.trim().replace('Change ', '')
3647
const parts: string[] = []
48+
const stream = this.stream()
49+
const change = this.changelist()
3750
if (stream) {
3851
parts.push(stream)
3952
}
@@ -42,11 +55,9 @@ export class Perforce extends Base {
4255
}
4356
return parts.join('/')
4457
}
45-
4658
safeRef(): string {
47-
return this.ref().split('//').filter(Boolean).join('-').replace(/\//g, '-')
59+
return this.ref().split('//').filter(Boolean).join('-').replace(/\//g, '-').toLowerCase()
4860
}
49-
5061
clone({
5162
source,
5263
destination,
@@ -74,12 +85,15 @@ export class Perforce extends Base {
7485

7586
export class Git extends Base {
7687
executable: string = 'git'
77-
private branch(): string {
78-
return execSync(this.executable, ['branch', '--show-current'], { cwd: this.cwd, quiet: false }).output.trim()
88+
branch(): string {
89+
return execSync(this.executable, ['branch', '--show-current'], { cwd: this.cwd, quiet: true }).output.trim()
90+
}
91+
commit(): string {
92+
return execSync(this.executable, ['rev-parse', 'HEAD'], { cwd: this.cwd, quiet: true }).output.trim()
7993
}
8094
ref(): string {
8195
const branch = this.branch()
82-
const commit = execSync(this.executable, ['rev-parse', 'HEAD'], { cwd: this.cwd, quiet: false }).output.trim()
96+
const commit = this.commit()
8397
const parts: string[] = []
8498
if (branch) {
8599
parts.push(branch)
@@ -89,7 +103,6 @@ export class Git extends Base {
89103
}
90104
return parts.join('/')
91105
}
92-
93106
clone(opts: CloneOpts): string {
94107
const { source, destination, branch, dryRun } = opts
95108
const cmd = branch ? ['clone', '-b', branch, source, destination] : ['clone', source, destination]

src/lib/template.ts

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,10 @@
11
import { RunrealConfig } from './types.ts'
22

3-
// This helper function will take a command string with placeholders and a substitutions object
4-
// It will replace all placeholders in the command with their corresponding values
5-
// If the key is not found in substitutions, keep the original placeholder
6-
export function render(input: string[], cfg: RunrealConfig) {
7-
// This regular expression matches all occurrences of ${placeholder}
8-
const substitutions: Record<string, string | undefined> = getSubstitutions(cfg)
9-
10-
const placeholderRegex = /\$\{([^}]+)\}/g
11-
return input.map((arg) =>
12-
arg.replace(placeholderRegex, (_, key: string) => {
13-
return key in substitutions ? substitutions[key] || key : _
14-
})
15-
)
16-
}
17-
18-
// Object containing the allowed substitutions
3+
/**
4+
* Get the substitutions object with values from the config.
5+
* @param {RunrealConfig} cfg
6+
* @returns {Record<string, string | undefined>} the substitutions object
7+
*/
198
export const getSubstitutions = (cfg: RunrealConfig): Record<string, string | undefined> => ({
209
'engine.path': cfg.engine?.path,
2110
'project.path': cfg.project?.path,
@@ -26,3 +15,60 @@ export const getSubstitutions = (cfg: RunrealConfig): Record<string, string | un
2615
'build.commit': cfg.build?.commitShort,
2716
'buildkite.buildNumber': cfg.buildkite?.buildNumber,
2817
})
18+
19+
/**
20+
* Regular expression for matching ${placeholders}.
21+
*/
22+
const placeholderRegex = /\$\{([^}]+)\}/g
23+
24+
/**
25+
* Replace all ${placeholders} in the items with values from the substitutions object.
26+
* If a placeholder is not found in the substitutions object, it will be kept as is.
27+
* @param {string[]} input
28+
* @param {RunrealConfig} cfg
29+
* @returns {string[]} the rendered items
30+
*/
31+
export function render(input: string[], cfg: RunrealConfig): string[] {
32+
const substitutions: Record<string, string | undefined> = getSubstitutions(cfg)
33+
return input.map((arg) =>
34+
arg.replace(placeholderRegex, (_, key: string) => {
35+
return key in substitutions ? substitutions[key] || key : _
36+
})
37+
)
38+
}
39+
40+
/**
41+
* Replace all ${placeholders} in the items with values from the substitutions object.
42+
* If a placeholder is not found in the substitutions object, it will be kept as is.
43+
* @param {any} item
44+
* @param {Record<string, string | undefined>} substitutions
45+
* @returns {any} the rendered items
46+
*/
47+
function renderItems(item: any, substitutions: Record<string, string | undefined>): any {
48+
if (typeof item === 'string') {
49+
// Replace placeholders in strings
50+
return item.replace(/\$\{([^}]+)\}/g, (_, key: string) => substitutions[key] || _)
51+
} else if (Array.isArray(item)) {
52+
// Recursively process each item in an array
53+
return item.map((subItem) => renderItems(subItem, substitutions))
54+
} else if (typeof item === 'object' && item !== null) {
55+
// Recursively process each property in an object
56+
const result: Record<string, any> = {}
57+
for (const key of Object.keys(item)) {
58+
result[key] = renderItems(item[key], substitutions)
59+
}
60+
return result
61+
}
62+
// Return the item as is if it's not a string, array, or object
63+
return item
64+
}
65+
66+
/**
67+
* Render the config by replacing all ${placeholders} with values from the substitutions object.
68+
* @param {RunrealConfig} cfg
69+
* @returns {RunrealConfig} rendered RunrealConfig
70+
*/
71+
export function renderConfig(cfg: RunrealConfig) {
72+
const substitutions: Record<string, string | undefined> = getSubstitutions(cfg)
73+
return renderItems(cfg, substitutions) as RunrealConfig
74+
}

src/lib/template_test.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

tests/config.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { assert, assertEquals } from 'https://deno.land/std/assert/mod.ts'
2+
import { Config } from '../src/lib/config.ts'
3+
import { ulid } from '../src/deps.ts'
4+
import { CliOptions } from '../src/lib/types.ts'
5+
6+
Deno.test('Config.create should initialize with default values', async () => {
7+
const config = await Config.create()
8+
const id = ulid()
9+
config.determineBuildId = () => id
10+
const expected = {
11+
engine: {
12+
path: '',
13+
repoType: 'git',
14+
},
15+
project: {
16+
name: '',
17+
path: '',
18+
repoType: 'git',
19+
},
20+
build: {
21+
id,
22+
path: '',
23+
branch: '',
24+
branchSafe: '',
25+
commit: '',
26+
commitShort: '',
27+
},
28+
buildkite: {
29+
branch: '',
30+
checkout: '',
31+
buildNumber: '0',
32+
buildCheckoutPath: Deno.cwd(),
33+
buildPipelineSlug: '',
34+
},
35+
metadata: {
36+
test: '',
37+
},
38+
workflows: [],
39+
}
40+
assertEquals(config.get(), expected)
41+
})
42+
43+
Deno.test('Config.create should load environment variables', async () => {
44+
Deno.env.set('RUNREAL_BUILD_ID', 'test-id')
45+
const config = await Config.create()
46+
assertEquals(config.get().build.id, 'test-id')
47+
Deno.env.delete('RUNREAL_BUILD_ID')
48+
})
49+
50+
Deno.test('Config.get should apply CLI options', async () => {
51+
const config = await Config.create()
52+
const id = ulid()
53+
config.determineBuildId = () => id
54+
const cliOptions: CliOptions = {
55+
enginePath: '/path/to/engine' as any,
56+
projectPath: '/path/to/project' as any,
57+
}
58+
const result = config.get(cliOptions)
59+
assert(result.engine.path.includes('/path/to/engine'))
60+
assert(result.project.path.includes('/path/to/project'))
61+
})

0 commit comments

Comments
 (0)