Shipping FiveM resources without a structured workflow leads to broken servers, angry players, and late-night rollbacks. Whether you're a solo developer or part of a team, a repeatable workflow saves time and prevents disasters.
This guide covers the full pipeline: version control with Git, automated testing, CI/CD integration, and zero-downtime deployment.
Git Branching Strategy
Use a simplified Git Flow tailored for FiveM development.
main ──●────────────●──────────●──
\ / \ /
develop ●──●──●──● ●──●──●─
\ / \
feature/ ●──● ●──●
Branch Rules
main— production. Merges only fromdevelop. Tag every merge with a release numberdevelop— integration branch. All features merge here firstfeature/<name>— individual features. Branch fromdevelop, merge backhotfix/<name>— emergency fixes. Branch frommain, merge to bothmainanddevelop
Commit Convention
Standardize commit messages for automatic changelogs:
type(scope): description
feat(resource): add bank robbery alarm system
fix(shop): prevent negative price exploit
refactor(core): extract payment logic to shared module
docs(install): update ox_lib requirement
.gitignore for FiveM Projects
# FiveM
cfx-server-data/
server-data/
[YOUR_SERVER].cfg
*.log
# IDE
.vscode/
.idea/
*.swp
# OS
.DS_Store
Thumbs.db
# Dependencies
[resources]/[framework]/
node_modules/
Resource Versioning
Every resource should have a meaningful version string. Use semantic versioning in your fxmanifest.lua:
fx_version 'cerulean'
game 'gta5'
author 'Your Name'
description 'qb-banking: Comprehensive banking system'
version '2.1.0'
Tag releases in Git:
git tag -a v2.1.0 -m "Release v2.1.0 - Add loan system and payment scheduler"
git push --tags
Automated Testing with qb-test
QBCore's built-in testing framework (qb-test) lets you run integration tests inside a headless server environment.
Writing Tests
-- tests/test_banking.lua
local QBCore = exports['qb-core']:GetCoreObject()
return {
name = "Banking System Tests",
before = function()
-- Setup: create a test character with known money
local Player = QBCore.Functions.GetPlayer(1)
Player.Functions.SetMoney("bank", 10000, "test-setup")
Player.Functions.SetMoney("cash", 500, "test-setup")
end,
["Should deposit cash to bank"] = function()
local Player = QBCore.Functions.GetPlayer(1)
local result = Player.Functions.AddMoney("bank", 500, "deposit")
assert(result == true, "Deposit should succeed")
assert(Player.PlayerData.money["bank"] == 10500, "Bank balance should be 10500")
assert(Player.PlayerData.money["cash"] == 0, "Cash should be deducted")
end,
["Should reject overdraft withdrawal"] = function()
local Player = QBCore.Functions.GetPlayer(1)
local result = Player.Functions.RemoveMoney("cash", 999999, "test")
assert(result == false, "Overdraft should fail")
assert(Player.PlayerData.money["cash"] == 500, "Cash should remain unchanged")
end,
}
Running Tests
# Run all tests
node qb-test/run.js
# Run specific test file
node qb-test/run.js tests/test_banking.lua
# Run with verbose output
node qb-test/run.js --verbose
What to Test
| Test type | Example |
|---|---|
| Money operations | Deposit, withdraw, transfer, overflow |
| Inventory | Add item, remove item, stack limits |
| Permissions | Job gates, admin checks, ownership |
| Edge cases | Negative amounts, zero quantities, missing players |
CI/CD with GitHub Actions
Automate testing and deployment so you ship confidently.
GitHub Actions Workflow
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [develop, main]
pull_request:
branches: [develop]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup FiveM server
run: |
wget https://runtime.fivem.net/artifacts/fivem/build_proot_linux/master/LATEST_VERSION
# Extract and configure headless server
- name: Run tests
run: node qb-test/run.js --ci
- name: Lint Lua files
uses: lunarmodules/luacheck@v1
with:
args: . --global
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to production
run: |
rsync -avz --delete \
--exclude '.git' \
--exclude 'tests' \
./ root@${{ secrets.SERVER_IP }}:/path/to/resources/
ssh root@${{ secrets.SERVER_IP }} 'screen -S fivem -X stuff "ensure qb-banking\r"'
Luacheck Configuration
-- .luacheckrc
globals = {
"QBCore",
"ESX",
"Citizen",
"RegisterNetEvent",
"AddEventHandler",
"TriggerEvent",
"TriggerClientEvent",
"MySQL",
"exports",
}
Zero-Downtime Deployment
FiveM doesn't support hot-reloading resources gracefully. Use this pattern to minimize disruption:
- Prepare on staging — deploy to a staging server first
- Run integration tests — verify the resource works with live data
- Sync files — rsync to production (this takes ~10 seconds)
- Restart with
ensure— use a screen session to ensure the resource - Monitor logs — watch for errors for 5 minutes after deployment
#!/bin/bash
# deploy.sh
RESOURCE="qb-banking"
SERVER="[email protected]"
REMOTE_PATH="/path/to/server-data/resources"
echo "=== Deploying $RESOURCE ==="
# Step 1: Sync files (exclude tests)
rsync -avz --delete \
--exclude '.git' \
--exclude 'tests' \
--exclude '.github' \
./ $SERVER:$REMOTE_PATH/$RESOURCE/
# Step 2: Ensure on production
ssh $SERVER "screen -S fivem -X stuff \"ensure $RESOURCE\r\""
echo "=== Deployed $RESOURCE ==="
Database Migrations
Track schema changes in version control. Use a simple migration script:
-- migrations/001_add_bank_accounts.sql
CREATE TABLE IF NOT EXISTS bank_accounts (
id INT AUTO_INCREMENT PRIMARY KEY,
citizenid VARCHAR(50) NOT NULL,
account_type VARCHAR(20) DEFAULT 'checking',
balance INT DEFAULT 0,
is_frozen BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_account (citizenid, account_type)
);
Run migrations on resource start:
-- server.lua
local function RunMigrations()
local migrations = {
"001_add_bank_accounts.sql",
"002_add_transaction_log.sql",
}
for _, file in ipairs(migrations) do
local version = file:match("^(%d+)")
local result = MySQL.scalar.await("SELECT COUNT(*) FROM schema_migrations WHERE version = ?", { version })
if result == 0 then
local sql = LoadResourceFile(GetCurrentResourceName(), "migrations/" .. file)
MySQL.query(sql)
MySQL.insert("INSERT INTO schema_migrations (version, applied_at) VALUES (?, NOW())", { version })
print("^2[Migration] Applied " .. file)
end
end
end
Monitoring Production
After deployment, monitor key metrics:
- Server FPS — should not drop after your resource loads
- Database query time — slow queries indicate missing indexes
- Player spawn time — your resource should not delay spawn
- Error logs — watch for unhandled events or nil values
Use FiveM's built-in performance metrics:
Citizen.CreateThread(function()
while true do
Citizen.Wait(60000)
local fps = GetServerFps()
local players = GetNumPlayerIndices()
print(("^3[Monitor] FPS: %d | Players: %d"):format(fps, players))
if fps < 30 then
print("^1[Monitor] WARNING: Server performance critical!")
end
end
end)
Summary Checklist
[ ] Git repository with main/develop branches
[ ] Semantic versioning in fxmanifest.lua
[ ] .gitignore configured for FiveM
[ ] Luacheck passing with no errors
[ ] qb-test or equivalent test suite
[ ] CI pipeline (GitHub Actions)
[ ] Deployment script with rsync
[ ] Database migration system
[ ] Production monitoring
Build your workflow once, and every resource you ship will be safer, faster, and more reliable.
Ziad is the lead developer of FiveForge. He builds AI systems that read codebases and get work done.
Stay updated
Get the latest AI development guides and FiveForge updates delivered to your inbox.
Build faster with FiveForge
An AI agent for complex work. Plan, build, debug, and automate projects — all from your desktop.