Appearance
Format Lint and Version Control
Written: 2026-10-14
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
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 ShopMathOne 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 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 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 mainStage 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
.envSave 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-capName 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.rbxlxInstall 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-layoutSelene lints patterns; it never formats layout. The Selene configuration 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
- Add a StyLua config and check a formatted file.
- Add a Selene config and clear one diagnostic.
- Draft a gitignore plus a small pull-request branch.
Solution 1
toml
syntax = "Luau"
column_width = 120
indent_type = "Tabs"
quote_style = "AutoPreferDouble"text
stylua --check src/shared/ShopMath.luauExpected behavior: the check passes on formatted code and rewrites only whitespace otherwise.
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.
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 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.