---
url: /tooling/format-lint-version-control.md
description: Format with StyLua, lint with Selene, and version Rojo projects with git.
---

# Format Lint and Version Control

*Written: {{ $frontmatter.date }}*

## Learning objectives

* You can separate formatting from linting.
* You can configure StyLua for Luau tabs.
* You can configure Selene with the right std.
* You can start a git repo for a Rojo project.
* You can write small reviewable pull requests.
* You can lock builds for reproducibility.

## Prerequisites

* [Rojo Project Structure](../intermediate/rojo-project-structure.md)
* [Testing](./testing.md)
* [Debugging Workflows](./debugging-workflows.md)

## Format with StyLua, lint with Selene

StyLua formats layout only. Selene flags risky patterns. Formatting never changes behavior. Linting never rewrites files.

StyLua owns quotes, tabs, widths, and trailing commas. Selene owns undefined globals, unused locals, and std mismatches. Run both before committing. The type solver and Script Analysis still own type errors; neither tool typechecks.

**ModuleScript in ReplicatedStorage named ShopMath**

```luau
--!strict
local ShopMath = {}

function ShopMath.total(price: number, count: number): number
	if not math.isfinite(price) then
		return 0
	end
	if not math.isfinite(count) then
		return 0
	end
	if count < 1 or count > 5 then
		return 0
	end
	return math.floor(price * count)
end

return ShopMath
```

One statement per line. Guard before computing. StyLua wraps the layout. Selene checks the globals. Expected behavior: both tools pass and `total(5, 2)` returns `10`.

## Configure StyLua once

Pin Luau syntax, tab indent, and a wide column in `stylua.toml`. Commit the file. Every machine formats identically. The [StyLua](https://github.com/JohnnyMorganz/StyLua) readme documents each key, so verify new keys there before adding them.

```toml
syntax = "Luau"
column_width = 120
indent_type = "Tabs"
quote_style = "AutoPreferDouble"
call_parentheses = "Always"
```

Set syntax once. Keep tabs for Roblox sources. Prefer double quotes with automatic fallback. Always keep call parentheses. Expected behavior: `stylua --check src/` passes on a formatted tree and rewrites only layout on `stylua src/`.

## Configure Selene once

Pin the Roblox std and scope the tree in `selene.toml`. Commit the file. CI lints the same tree. The [Selene configuration](https://kampfkarren.github.io/selene/usage/configuration.html) reference documents each key, so verify new keys there before adding them.

```toml
std = "roblox"
```

Start with the Roblox std. Add excludes only for generated folders like Packages. Fix every diagnostic in touched files. Expected behavior: `selene src/` prints zero errors on a clean tree.

## Start git for a Rojo project

Init once. Ignore builds and installs. Commit source, configs, and lockfiles. Push to a remote and pull before branching.

Never commit place builds, sourcemaps, Package folders, or secrets. Those artifacts regenerate. Source plus lockfiles rebuild them.

```text
git init
git add stylua.toml selene.toml default.project.json wally.toml src/
git commit -m "Add shop service and shared math"
git push -u origin main
```

Stage configs and sources explicitly on the first commit. Write an imperative message. Set upstream once. Expected behavior: a fresh clone plus installs rebuilds the same place.

```text
game.rbxlx
sourcemap.json
Packages/
ServerPackages/
.wally/
*.rbxm
.env
```

Save those lines as `.gitignore` in the project root. Keep `wally.lock` tracked for reproducibility. Expected behavior: status stays clean after builds and installs.

## Open small pull requests

Branch per fix. Keep the diff under a few hundred lines. Describe behavior, tests, and risks. Link the repro or the spec run.

Ask for one reviewer on small changes. Merge only when StyLua, Selene, and a Play smoke test pass. Delete the branch after merge so the list stays short.

```text
git checkout -b fix/shop-count-cap
git add src/shared/ShopMath.luau
git commit -m "Cap shop counts at five"
git push -u origin fix/shop-count-cap
```

Name the branch after the fix. Stage only the intended file. Push for review. Expected behavior: the request shows one intent, one test note, and a clean diff.

## Lock reproducible builds

Pin tools and packages. Commit `wally.lock` and Rokit pins. Regenerate the sourcemap after adds. Build from a clean tree before release.

Lockfiles record exact versions. Reviews diff the lock change beside the code change. Rollbacks restore both together. Unpinned builds drift across machines.

```text
wally install
rojo sourcemap --output sourcemap.json
rojo build -o game.rbxlx
```

Install first. Refresh types second. Build last. Expected behavior: the same commit builds the same place on any machine with the same toolchain.

## Common mistakes

**Mistake 1: you lint to format.**

Wrong command below. It expects Selene to rewrite layout.

```text
selene src/ --fix-layout
```

Selene lints patterns; it never formats layout. The [Selene configuration](https://kampfkarren.github.io/selene/usage/configuration.html) reference lists diagnostics, not formatters. Fix it by running `stylua src/` for layout and `selene src/` for diagnostics.

**Mistake 2: you commit builds and Packages.**

Wrong add below. It tracks regenerable output.

```text
git add game.rbxlx Packages/ ServerPackages/
```

It bloats history and it conflicts on every build. Fix it with the `.gitignore` and by tracking `src/`, configs, and `wally.lock` only.

**Mistake 3: you open a giant review.**

Wrong branch below. It mixes format, shop, and rounds.

```text
git checkout -b big/update
git add .
git commit -m "Stuff"
```

No reviewer can verify three intents at once. Fix it with one branch per intent, an imperative message, and a test note like `ShopMath green`.

## Exercises

1. Add a StyLua config and check a formatted file.
2. Add a Selene config and clear one diagnostic.
3. Draft a gitignore plus a small pull-request branch.

::: details Solution 1

```toml
syntax = "Luau"
column_width = 120
indent_type = "Tabs"
quote_style = "AutoPreferDouble"
```

```text
stylua --check src/shared/ShopMath.luau
```

Expected behavior: the check passes on formatted code and rewrites only whitespace otherwise.
:::

::: details Solution 2

```toml
std = "roblox"
```

**Script in ServerScriptService**

```luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local ShopMath = require(ReplicatedStorage:WaitForChild("ShopMath"))
print(ShopMath.total(5, 2))
```

Expected behavior: `selene src/` reports no undefined-global or unused warnings for this file.
:::

::: details Solution 3

```text
game.rbxlx
sourcemap.json
Packages/
ServerPackages/
```

```text
git checkout -b fix/shop-fee-math
git add src/shared/ShopMath.luau
git commit -m "Fix shop fee rounding"
```

Expected behavior: the branch holds one fix with builds ignored and lockfiles still tracked.
:::

## Pin the formatter and the linter

Defaults drift between machines; checked-in configs do not. StyLua reads `.stylua.toml` from the formatted file's directory upward (then `.editorconfig`, then defaults), so keep one at the repo root. Set `syntax = "Luau"` to disambiguate, and pick `column_width`, `indent_type`, and `quote_style` once, the team never debates them again. Selene reads `selene.toml` with `std = "roblox"` so `game` and Engine globals resolve, plus the auto-generated Roblox standard library (refresh with `selene update-roblox-std`, or pin it with `roblox-std-source = "pinned"`). Gate CI on `stylua --check src/` and `selene src/` so unformatted or flagged code never merges.

```toml
syntax = "Luau"
column_width = 120
indent_type = "Tabs"
indent_width = 4
quote_style = "AutoPreferDouble"
```

```toml
std = "roblox"
```

## Headless tasks with Lune

StyLua and Selene check code; [Lune](https://lune-org.github.io/docs/) runs Luau off-Engine for headless chores: migrations, asset transforms, and CI checks without opening Studio. Run scripts with `lune run tools/check` (`.luau` extension implied), and import capabilities through the `@lune/` prefix, `require("@lune/fs")` for files, `require("@lune/net")` for web calls, `require("@lune/process")` for spawning tools. Keep Lune scripts under a `tools/` folder, out of Rojo sync roots, and pin the Lune version alongside your other tools.

```luau
--!strict
local fs = require("@lune/fs")

local files = fs.readDir("src/shared")
print(#files)
```

## Summary

Format with StyLua and lint with Selene. Pin both configs at the repo root and gate CI on both checks. Run headless chores with Lune outside sync roots. Ignore builds and installs. Review small branches with passing checks. Lock versions so rebuilds match. Next, run the [Shipping Checklist](./shipping-checklist.md).
