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
145 changes: 103 additions & 42 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { deepmerge, dotenv, parse, path, ulid, ValidationError, z } from '../dep
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 { Git, Perforce, Source } from './source.ts'
import { renderConfig } from './template.ts'

const env = (key: string) => Deno.env.get(key) || ''
Expand All @@ -16,15 +16,11 @@ export class Config {
project: {
name: '',
path: '',
buildPath: '',
repoType: 'git',
},
build: {
path: '',
id: env('RUNREAL_BUILD_ID') || '',
branch: '',
branchSafe: '',
commit: '',
commitShort: '',
},
buildkite: {
branch: env('BUILDKITE_BRANCH') || '',
Expand All @@ -34,7 +30,17 @@ export class Config {
buildPipelineSlug: env('BUILDKITE_PIPELINE_SLUG') || '',
},
metadata: {
test: env('RUNREAL_BUILD_ID') || '',
safeRef: '',
git: {
branch: '',
branchSafe: '',
commit: '',
commitShort: '',
},
perforce: {
changelist: '',
stream: '',
},
},
workflows: [],
}
Expand All @@ -43,7 +49,7 @@ export class Config {
'branch': 'engine.branch',
'cachePath': 'engine.cachePath',
'projectPath': 'project.path',
'buildPath': 'build.path',
'buildPath': 'project.buildPath',
'buildId': 'build.id',
'gitDependenciesCachePath': 'git.dependenciesCachePath',
'gitMirrors': 'git.mirrors',
Expand Down Expand Up @@ -82,17 +88,6 @@ export class Config {
return this.config
}

determineBuildId() {
const build = this.config.build
if (!build) return ulid()
if (!this.config.project?.path) return ulid()
if (!this.config.project?.repoType) return ulid()
const source = Source(this.config.project?.path, this.config.project?.repoType)
const safeRef = source.safeRef()
if (!safeRef) return ulid()
return safeRef
}

private async searchForConfigFile(): Promise<string | null> {
const cwd = Deno.cwd()
const configPath = path.join(cwd, 'runreal.config.json')
Expand Down Expand Up @@ -143,8 +138,8 @@ export class Config {
config.project.path = path.resolve(config.project.path)
}

if (config.build && config.build.path) {
config.build.path = path.resolve(config.build.path)
if (config.project && config.project.buildPath) {
config.project.buildPath = path.resolve(config.project.buildPath)
}

if (config.git && config.git.dependenciesCachePath) {
Expand All @@ -158,31 +153,91 @@ export class Config {
return config
}

private populateBuild(): RunrealConfig['build'] | null {
private getBuildMetadata(): RunrealConfig['metadata'] | null {
const cwd = this.config.project?.path
if (!cwd) return null
if (this.config.project?.repoType === 'git') {
const { safeRef, git } = this.getGitBuildMetadata(cwd)
return {
safeRef,
git,
}
} else if (this.config.project?.repoType === 'perforce') {
const { safeRef, perforce } = this.getPerforceBuildMetadata(cwd)
return {
safeRef,
perforce,
}
}
return null
}

private getGitBuildMetadata(projectPath: string): RunrealConfig['metadata'] {
const cwd = projectPath
try {
let branch: string
// On Buildkite, use the BUILDKITE_BRANCH env var as we may be in a detached HEAD state
if (Deno.env.get('BUILDKITE_BRANCH')) {
branch = this.config.buildkite?.branch || ''
} else {
branch = execSync('git', ['branch', '--show-current'], { cwd, quiet: false }).output.trim()
const source = new Git(cwd)
const branch = source.branch()
const branchSafe = source.branchSafe()
const commit = source.commit()
const commitShort = source.commitShort()
const safeRef = source.safeRef()
return {
safeRef,
git: {
branch,
branchSafe,
commit,
commitShort,
},
}
} catch (e) {
return {
safeRef: '',
git: {
branch: '',
branchSafe: '',
commit: '',
commitShort: '',
},
}
const branchSafe = branch.replace(/[^a-z0-9]/gi, '-')
const commit = execSync('git', ['rev-parse', 'HEAD'], { cwd, quiet: false }).output.trim()
const commitShort = execSync('git', ['rev-parse', '--short', 'HEAD'], { quiet: false }).output.trim()
}
}

private getPerforceBuildMetadata(projectPath: string): RunrealConfig['metadata'] {
const cwd = projectPath
try {
const source = new Perforce(cwd)
const changelist = source.changelist()
const stream = source.stream()
const safeRef = source.safeRef()
return {
...this.config.build,
path: this.config.build?.path || '',
branch,
branchSafe,
commit,
commitShort,
safeRef,
perforce: {
changelist,
stream,
},
}
} catch (e) {
return null
return {
safeRef: '',
perforce: {
changelist: '',
stream: '',
},
}
}
}

getBuildId() {
if (this.config.build?.id) return this.config.build.id
if (!this.config.project?.path) return ulid()
if (!this.config.project?.repoType) return ulid()
try {
const source = Source(this.config.project.path, this.config.project.repoType)
const safeRef = source.safeRef()
return safeRef
} catch (e) {
return ulid()
}
}

Expand All @@ -192,10 +247,16 @@ export class Config {
const Merged = ConfigSchema.and(InternalSchema)
this.config = Merged.parse(this.config)

const bd = this.populateBuild()
if (bd) this.config.build = bd
if (!this.config.build?.id) {
this.config.build!.id = this.determineBuildId()
const metadata = this.getBuildMetadata()
this.config.metadata = {
...this.config.metadata,
...metadata,
}

const buildId = this.getBuildId()
this.config.build = {
...this.config.build,
id: buildId,
}
} catch (e) {
if (e instanceof z.ZodError) {
Expand Down
38 changes: 20 additions & 18 deletions src/lib/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,18 @@ export const InternalSchema = z.object({
buildPipelineSlug: z.string().describe('Buildkite pipeline slug').optional(),
}).optional(),
metadata: z.object({
test: z.string().describe('Build id <RUNREAL_BUILD_ID>'),
}).optional(),
safeRef: z.string().describe('Safe reference for file outputs or build ids').optional(),
git: z.object({
branch: z.string().describe('Branch name'),
branchSafe: z.string().describe('Safe branch name'),
commit: z.string().describe('Commit hash'),
commitShort: z.string().describe('Short commit hash'),
}).optional(),
perforce: z.object({
stream: z.string().describe('Stream name'),
changelist: z.string().describe('Changelist number'),
}).optional(),
}),
})

export const ConfigSchema = z.object({
Expand All @@ -27,28 +37,20 @@ export const ConfigSchema = z.object({
project: z.object({
name: z.string().optional().describe('Project name'),
path: z.string().describe('Path to the project folder <RUNREAL_PROJECT_PATH>'),
buildPath: z.string().describe('Path to the build folder <RUNREAL_BUILD_PATH>'),
repoType: z.string().describe('git or perforce'),
}),
build: z.object({
path: z.string().describe('Path to the build folder <RUNREAL_BUILD_PATH>'),
id: z.string().optional().describe('Build id <RUNREAL_BUILD_ID>'),
branch: z.string().optional().describe('Branch name'),
branchSafe: z
}),
git: z.object({
dependenciesCachePath: z
.string()
.optional()
.describe('Branch name safe for filenames'),
commit: z.string().optional().describe('Commit hash'),
commitShort: z.string().optional().describe('Short commit hash'),
}),
git: z
.object({
dependenciesCachePath: z
.string()
.optional()
.describe('Path to git dependencies cache folder <RUNREAL_GIT_DEPENDENCIES_CACHE_PATH>'),
mirrors: z.boolean().optional().describe('Use git mirrors'),
mirrorsPath: z.string().optional().describe('Path to git mirrors folder <RUNREAL_GIT_MIRRORS_PATH>'),
})
.describe('Path to git dependencies cache folder <RUNREAL_GIT_DEPENDENCIES_CACHE_PATH>'),
mirrors: z.boolean().optional().describe('Use git mirrors'),
mirrorsPath: z.string().optional().describe('Path to git mirrors folder <RUNREAL_GIT_MIRRORS_PATH>'),
})
.optional(),
workflows: z.array(
z.object({
Expand Down
14 changes: 12 additions & 2 deletions src/lib/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,24 @@ export class Perforce extends Base {
export class Git extends Base {
executable: string = 'git'
branch(): string {
// On Buildkite, use the BUILDKITE_BRANCH env var as we may be in a detached HEAD state
if (Deno.env.get('BUILDKITE_BRANCH')) {
return Deno.env.get('BUILDKITE_BRANCH') || ''
}
return execSync(this.executable, ['branch', '--show-current'], { cwd: this.cwd, quiet: true }).output.trim()
}
branchSafe(): string {
return this.branch().replace(/[^a-z0-9]/gi, '-')
}
commit(): string {
return execSync(this.executable, ['rev-parse', 'HEAD'], { cwd: this.cwd, quiet: true }).output.trim()
}
commitShort(): string {
return execSync(this.executable, ['rev-parse', '--short', 'HEAD'], { cwd: this.cwd, quiet: true }).output.trim()
}
ref(): string {
const branch = this.branch()
const commit = this.commit()
const branch = this.branchSafe()
const commit = this.commitShort()
const parts: string[] = []
if (branch) {
parts.push(branch)
Expand Down
10 changes: 7 additions & 3 deletions src/lib/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ export const getSubstitutions = (cfg: RunrealConfig): Record<string, string | un
'engine.path': cfg.engine?.path,
'project.path': cfg.project?.path,
'project.name': cfg.project?.name,
'project.buildPath': cfg.project?.buildPath,
'build.path': cfg.project?.buildPath,
'build.id': cfg.build?.id,
'build.path': cfg.build?.path,
'build.branch': cfg.build?.branchSafe,
'build.commit': cfg.build?.commitShort,
'metadata.safeRef': cfg.metadata?.safeRef,
'metadata.git.branch': cfg.metadata?.git?.branchSafe,
'metadata.git.commit': cfg.metadata?.git?.commitShort,
'metadata.perforce.stream': cfg.metadata?.perforce?.stream,
'metadata.perforce.changelist': cfg.metadata?.perforce?.changelist,
'buildkite.buildNumber': cfg.buildkite?.buildNumber,
})

Expand Down
34 changes: 21 additions & 13 deletions tests/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
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 { path, 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
config.getBuildId = () => id
const expected = {
engine: {
path: '',
Expand All @@ -15,15 +15,11 @@ Deno.test('Config.create should initialize with default values', async () => {
project: {
name: '',
path: '',
buildPath: '',
repoType: 'git',
},
build: {
id,
path: '',
branch: '',
branchSafe: '',
commit: '',
commitShort: '',
},
buildkite: {
branch: '',
Expand All @@ -33,7 +29,17 @@ Deno.test('Config.create should initialize with default values', async () => {
buildPipelineSlug: '',
},
metadata: {
test: '',
safeRef: '',
git: {
branch: '',
branchSafe: '',
commit: '',
commitShort: '',
},
perforce: {
stream: '',
changelist: '',
},
},
workflows: [],
}
Expand All @@ -50,12 +56,14 @@ Deno.test('Config.create should load environment variables', async () => {
Deno.test('Config.get should apply CLI options', async () => {
const config = await Config.create()
const id = ulid()
config.determineBuildId = () => id
config.getBuildId = () => id
const enginePath = path.normalize('/path/to/engine')
const projectPath = path.normalize('/path/to/project')
const cliOptions: CliOptions = {
enginePath: '/path/to/engine' as any,
projectPath: '/path/to/project' as any,
enginePath: enginePath as any,
projectPath: projectPath as any,
}
const result = config.get(cliOptions)
assert(result.engine.path.includes('/path/to/engine'))
assert(result.project.path.includes('/path/to/project'))
assert(result.engine.path.includes(enginePath))
assert(result.project.path.includes(projectPath))
})
Loading