Skip to content

Unified Configuration

Sky tools support unified configuration through project-level config files. Define consistent settings for skytest, skylint, and other tools without repeating flags on every invocation.

Sky supports two configuration formats:

FormatFileBest For
TOMLsky.tomlSimple, static configuration
Starlarkconfig.sky (canonical) or sky.star (legacy)Dynamic, conditional configuration

Create a sky.toml in your project root:

[test]
timeout = "60s"
parallel = "auto"
prelude = ["test/helpers.star"]

Sky automatically discovers configuration files by walking up the directory tree from your current working directory. The search stops at the repository root (the directory containing .git).

  1. CLI flag: --config=path/to/config.sky (highest priority)
  2. Environment variable: SKY_CONFIG=/path/to/config.sky
  3. Directory walk: Starting from current directory, moving up to repository root
  1. config.sky (canonical Starlark config)
  2. sky.star (legacy Starlark config)
  3. sky.toml (TOML config)

If no config file is found, sensible defaults are used.

my-project/
├── .git/
├── config.sky # <- Found and used
├── src/
│ └── lib.star
└── tests/
└── lib_test.star # Running skytest here uses ../config.sky
Terminal window
# Use a specific config file
skytest --config=ci-config.sky tests/
# Use a custom Starlark execution timeout (default: 5s)
skytest --config=config.sky --config-timeout=10s tests/
Terminal window
# Set config path via environment
export SKY_CONFIG=/path/to/config.sky
skytest tests/
# Override per-command
SKY_CONFIG=local.sky skytest tests/

CLI flags always override config file settings:

  1. CLI flags (highest)
  2. Config file settings
  3. Built-in defaults (lowest)
# sky.toml - Full configuration example
[test]
# Per-test timeout (Go duration format: "30s", "1m", "1h30m")
timeout = "60s"
# Parallelism: "auto" (use all CPUs), "1" (sequential), or a number
parallel = "auto"
# Prelude files loaded before each test file
prelude = ["test/helpers.star", "test/fixtures.star"]
# Test function prefix (default: "test_")
prefix = "test_"
# Stop on first test failure
fail_fast = false
# Enable verbose output
verbose = false
[test.coverage]
# Enable coverage collection (EXPERIMENTAL)
enabled = false
# Fail if coverage falls below this percentage
fail_under = 80.0
# Coverage output file path
output = "coverage.json"
[lint]
# Rules or categories to enable
enable = ["all"]
# Rules or patterns to disable
disable = ["native-*"]
# Treat warnings as errors
warnings_as_errors = false
OptionTypeDefaultDescription
timeoutstring"30s"Per-test timeout in Go duration format
parallelstring"" (sequential)"auto", "1", or a specific number
preludelist[]Prelude files to load before tests
prefixstring"test_"Test function name prefix
fail_fastboolfalseStop on first failure
verboseboolfalseEnable verbose output
OptionTypeDefaultDescription
enabledboolfalseEnable coverage collection
fail_underfloat0Minimum coverage percentage
outputstring"coverage.json"Coverage output file path
OptionTypeDefaultDescription
enablelist[]Rules or categories to enable
disablelist[]Rules or patterns to disable
warnings_as_errorsboolfalseTreat warnings as errors

Durations use Go’s duration format:

UnitExampleDescription
s"30s"Seconds
m"5m"Minutes
h"1h"Hours
Combined"1h30m"1 hour and 30 minutes
Combined"2m30s"2 minutes and 30 seconds

Starlark config files must define a configure() function that returns a dictionary.

def configure():
return {
"test": {
"timeout": "60s",
"parallel": "auto",
},
"lint": {
"enable": ["all"],
},
}

Starlark config files have access to these predeclared values and functions:

BuiltinTypeDescription
getenv(name, default="")functionGet environment variable value
host_osstringCurrent OS ("darwin", "linux", "windows")
host_archstringCurrent architecture ("amd64", "arm64")
duration(s)functionValidate and return a duration string
struct(**kwargs)functionCreate a dict from keyword arguments

Returns the value of an environment variable, or the default if not set.

def configure():
ci = getenv("CI", "") != ""
github_actions = getenv("GITHUB_ACTIONS", "") == "true"
custom_timeout = getenv("TEST_TIMEOUT", "30s")
return {
"test": {
"timeout": custom_timeout,
"parallel": "1" if ci else "auto",
},
}

A string containing the current operating system. Values match Go’s runtime.GOOS:

  • "darwin" - macOS
  • "linux" - Linux
  • "windows" - Windows
def configure():
# Use more parallelism on Linux servers
parallel = "auto"
if host_os == "linux":
parallel = "8"
return {
"test": {
"parallel": parallel,
},
}

A string containing the current CPU architecture. Values match Go’s runtime.GOARCH:

  • "amd64" - x86-64
  • "arm64" - ARM64/Apple Silicon
def configure():
# Longer timeouts on emulated architectures
timeout = "30s"
if host_arch == "arm64" and host_os == "linux":
timeout = "60s" # Might be running under emulation
return {
"test": {
"timeout": timeout,
},
}

Validates that a string is a valid Go duration and returns it. Useful for catching typos early.

def configure():
# This will fail at config load time if the format is invalid
timeout = duration("60s")
return {
"test": {
"timeout": timeout,
},
}

Creates a dictionary from keyword arguments. Useful for readable nested configuration.

def configure():
return {
"test": struct(
timeout = "60s",
parallel = "auto",
coverage = struct(
enabled = True,
fail_under = 80,
),
),
}

Starlark config files run in a sandboxed environment:

  • No filesystem access (use getenv instead of reading files)
  • No network access
  • No module loading (load() is not available)
  • 5-second execution timeout by default (configurable via --config-timeout)
sky.toml
[test]
timeout = "30s"
verbose = true
# config.sky - Different settings for CI vs local development
def configure():
ci = getenv("CI", "") != ""
if ci:
return {
"test": {
# Longer timeout for CI (cold caches, shared resources)
"timeout": "120s",
# Sequential execution for deterministic results
"parallel": "1",
# Fail fast to save CI minutes
"fail_fast": True,
# Coverage required in CI
"coverage": {
"enabled": True,
"fail_under": 80,
},
},
}
else:
return {
"test": {
# Shorter timeout for local dev
"timeout": "30s",
# Use all cores locally
"parallel": "auto",
# See all failures at once
"fail_fast": False,
},
}
# config.sky - Platform-specific configuration
def configure():
timeout = "30s"
parallel = "auto"
# Windows often needs longer timeouts
if host_os == "windows":
timeout = "60s"
parallel = "4" # Windows handles fewer parallel processes well
# macOS with Apple Silicon is fast
if host_os == "darwin" and host_arch == "arm64":
timeout = "15s"
parallel = "auto"
return {
"test": {
"timeout": timeout,
"parallel": parallel,
},
}

Shared Prelude with Environment-Specific Overrides

Section titled “Shared Prelude with Environment-Specific Overrides”
# config.sky - Common prelude with environment tweaks
def configure():
# Common preludes for all environments
preludes = [
"test/helpers.star",
"test/fixtures.star",
]
# Add mock prelude in CI to avoid external dependencies
if getenv("CI", "") != "":
preludes.append("test/mocks.star")
return {
"test": {
"prelude": preludes,
"timeout": getenv("TEST_TIMEOUT", "30s"),
},
}
# config.sky - Different settings based on team conventions
def configure():
team = getenv("TEAM", "default")
configs = {
"platform": {
"timeout": "120s",
"prefix": "test_",
"fail_fast": False,
},
"frontend": {
"timeout": "30s",
"prefix": "spec_", # Different naming convention
"verbose": True,
},
"default": {
"timeout": "60s",
"prefix": "test_",
},
}
return {
"test": configs.get(team, configs["default"]),
}

Error: multiple config files found in the same directory; use only one

Cause: You have more than one of config.sky, sky.star, or sky.toml in the same directory.

Solution: Remove the extra config files. If migrating from sky.star to config.sky, delete the old file.

Terminal window
# Check for config files
ls config.sky sky.star sky.toml 2>/dev/null

Error: execution timeout when loading Starlark config

Cause: Your configure() function is taking too long, possibly due to an infinite loop.

Solution:

  1. Check for infinite loops in your config
  2. Increase the timeout: --config-timeout=10s
  3. Simplify complex computations
# BAD - infinite loop
def configure():
while True:
pass # Never returns
# GOOD - simple conditionals
def configure():
ci = getenv("CI", "") != ""
return {"test": {"timeout": "60s" if ci else "30s"}}

Error: invalid duration "60" or invalid duration "one minute"

Cause: Duration strings must use Go’s duration format.

Solution: Use proper duration format with unit suffixes.

# BAD
timeout = "60" # Missing unit
timeout = "one minute" # Not a valid format
# GOOD
timeout = "60s" # 60 seconds
timeout = "1m" # 1 minute
timeout = "1m30s" # 1 minute 30 seconds

Symptom: Default settings are used even though you have a config file.

Diagnostic steps:

  1. Run with verbose mode to see which config is used:

    Terminal window
    skytest -v tests/
    # Output: skytest: using config /path/to/config.sky
  2. Check your current directory:

    Terminal window
    pwd
    ls config.sky sky.star sky.toml
  3. Verify the config file is in the directory tree:

    Terminal window
    # Walk up looking for config
    while [ ! -f config.sky ] && [ ! -f sky.star ] && [ ! -f sky.toml ]; do
    cd ..
    [ "$PWD" = "/" ] && break
    done
    ls config.sky sky.star sky.toml 2>/dev/null

Error: executing config /path/to/config.sky: ...

Cause: Syntax error in your Starlark config file.

Solution: Check your Starlark syntax. Common issues:

# BAD - Python-style True/False must be capitalized
"verbose": true # Starlark uses True
# GOOD
"verbose": True
# BAD - missing comma
return {
"test": {
"timeout": "30s" # Missing comma
"parallel": "auto"
}
}
# GOOD
return {
"test": {
"timeout": "30s",
"parallel": "auto",
},
}

Error: configure() must return a dict, got string

Cause: Your configure() function returns something other than a dictionary.

Solution: Ensure you return a dictionary:

# BAD
def configure():
return "timeout=60s" # Returns string
# GOOD
def configure():
return {
"test": {
"timeout": "60s",
},
}
  1. Create sky.toml in your project root:

    [test]
    timeout = "30s"
  2. Remove flags from your test commands:

    Terminal window
    # Before
    skytest --timeout=30s tests/
    # After
    skytest tests/

If you need conditional logic:

  1. Create config.sky with equivalent settings:

    def configure():
    return {
    "test": {
    "timeout": "30s",
    # Add conditional logic as needed
    },
    }
  2. Delete sky.toml:

    Terminal window
    rm sky.toml

The sky.star filename is still supported but deprecated. To migrate:

Terminal window
mv sky.star config.sky

No changes to the file contents are needed - the format is identical.

  1. Start simple: Begin with sky.toml and migrate to config.sky only when you need conditional logic.

  2. Commit your config: Config files should be version controlled so all team members use the same settings.

  3. Document overrides: If team members need to override settings locally, document how:

    Terminal window
    # Override for local development
    SKY_CONFIG=local.sky skytest tests/
  4. Use CI detection: The CI environment variable is set by most CI systems (GitHub Actions, GitLab CI, CircleCI, etc.):

    ci = getenv("CI", "") != ""
  5. Keep configs fast: Avoid complex computations in configure(). The function should return quickly.

  6. Test your config: Run with -v to verify your config is being loaded correctly:

    Terminal window
    skytest -v tests/

Sky tools need to understand which “flavor” of Starlark you are using. Different tools extend vanilla Starlark with their own builtins, and sky needs to know about these extensions to provide accurate diagnostics.

A “dialect” describes how a specific tool extends the core Starlark language. This is similar to how Python frameworks extend Python:

Python ParallelStarlark Equivalent
Django templates (Python-like but not Python)Bazel BUILD files (Starlark with build rules)
Jinja2 templatesTiltfiles (Starlark with K8s functions)
NumPy array extensionsBuck2’s record and enum types

When sky tools analyze your Starlark files, they need to know:

  1. Which builtins exist: Is docker_build() a valid function? (Yes in Tilt, no in Bazel)
  2. What parameters are valid: Does cc_library have an hdrs or headers parameter?
  3. Which lint rules apply: Should we enforce Bazel-specific conventions?

The sky.toml configuration focuses on tool behavior (timeouts, parallelism, preludes), while dialect configuration in .starlark/config.json focuses on language understanding (builtins, type definitions).

# Tool behavior configuration
[test]
timeout = "60s"
parallel = "auto"
[lint]
enable = ["all"]
disable = ["native-*"]

Bazel monorepo - Default dialect detection usually works:

# sky.toml - no dialect config needed
# skyls auto-detects Bazel from WORKSPACE file
[lint]
enable = ["all"]

Tilt project - Need explicit dialect configuration:

.starlark/config.json
{
"version": 1,
"rules": [
{"files": ["Tiltfile"], "dialect": "tilt"}
]
}

Multi-tool monorepo - Map files to dialects:

.starlark/config.json
{
"version": 1,
"rules": [
{"files": ["Tiltfile"], "dialect": "tilt"},
{"files": ["*.bara.sky"], "dialect": "copybara"},
{"files": ["BUILD", "**/*.bzl"], "dialect": "bazel"}
]
}