What a Unity build needs before it can go on Steam with SteamPipe
ContentBuilder layout, app and depot VDFs, SteamCMD and Steam Guard, what to keep out of the depot, a git hash build stamp, and the same upload from CI.
Valve's upload system, SteamPipe, does not take a zip. It takes a folder layout, two small script files and a login through SteamCMD. Here is all of it with nothing but Unity, SteamCMD and a text editor.
The ContentBuilder layout
The Steamworks SDK ships a tools\ContentBuilder folder with these subfolders:
builder: holdssteamcmd.exe, the command-line Steam client. Run it once and it bootstraps the build system.builder_linuxandbuilder_osxhold the same tool for other platforms.content: the game files that become depots.output: build logs, chunk cache and intermediate output. You can delete it at any time; the next upload just takes longer.scripts: your app build and depot build scripts.
ContentRoot can point anywhere, so your Unity output folder can stay where it is.
The app build script
scripts\app_build_<appid>.vdf is a small key-value file. Valve's example, trimmed to what a Unity game needs:
"AppBuild"
{
"AppID" "1000"
"Desc" "Build from commit a1b2c3d"
"ContentRoot" "C:\Builds\MyGame-windows-client\"
"BuildOutput" "..\output\"
"SetLive" ""
"Depots"
{
"1001" "depot_build_1001.vdf"
}
}
What the fields mean, from Valve's documentation:
AppID: your app id. The uploading account needs the "Edit App Metadata" permission on it.Desc: an internal description, visible only in the Your Builds section of Steamworks. Put the git hash here.ContentRoot: the root folder of the game files, absolute or relative to the script.BuildOutput: where logs, manifests and chunk caches go.SetLive: a beta branch name to set live automatically after a successful build; empty means none. The default branch cannot be set live this way at all.Depots: either inline depot sections or a map from depot id to a separate depot script.
The optional Preview "1" writes logs and a manifest and uploads nothing.
The depot build script
"DepotBuild"
{
"DepotID" "1001"
"FileMapping"
{
"LocalPath" "*"
"DepotPath" "."
"Recursive" "1"
}
"FileExclusion" "*.pdb"
"FileExclusion" "*_BackUpThisFolder_ButDontShipItWithYourGame\*"
"FileExclusion" "*_BurstDebugInformation_DoNotShip\*"
}
DepotID: the depot this script fills. A depot script can set its ownContentRootto override the app-level one.FileMapping: maps files from the content root into the depot.LocalPathis relative to the content root and accepts?and*wildcards.DepotPathis where they land in the depot;.means the depot root.Recursive "1"makes the match apply in subfolders too.FileExclusion: removes files that a mapping matched. Wildcards work here as well; Valve's own example uses*.pdb.
Check exclusions with a preview build and its manifest before the real upload.
What must stay out of the depot
The Unity 6.3 manual lists what a Windows IL2CPP build produces. Next to MyGame.exe, MyGame_Data, UnityPlayer.dll and GameAssembly.dll there is a folder named MyGame_BackUpThisFolder_ButDontShipItWithYourGame. It holds the PDB files and the generated C++. The manual says to back it up for every build you ship and not redistribute it: you need it to read crash dumps, players do not.
Separate .pdb files appear when "Copy PDB files" is on in the Build Profiles window; WinPixEventRuntime.dll means a development build.
If your project uses the Burst package, it writes a <build>_BurstDebugInformation_DoNotShip folder alongside the data folder (Burst changelog, 1.6.0-pre.3 and 1.8.23). Same rule: keep it, do not ship it.
The three FileExclusion lines above cover all of this. Also build each release into an empty folder; SteamPipe uploads whatever matches, including leftovers from an earlier development build.
Stamp the build with the git hash
A bug report is only useful if you can tell which commit produced the build. Unity copies everything in Assets/StreamingAssets unmodified into the player output, and Application.streamingAssetsPath points at it at runtime. Write a small JSON file there before the build:
// Assets/Editor/Stamp.cs
using System.Diagnostics;
using System.IO;
public static class Stamp
{
public static void Write()
{
string hash = Git("rev-parse --short HEAD");
string dirty = Git("status --porcelain").Length > 0 ? "true" : "false";
Directory.CreateDirectory("Assets/StreamingAssets");
File.WriteAllText("Assets/StreamingAssets/build-info.json",
"{\"git\":\"" + hash + "\",\"dirty\":" + dirty + "}");
}
static string Git(string args)
{
var p = Process.Start(new ProcessStartInfo("git", args)
{ RedirectStandardOutput = true, UseShellExecute = false });
string s = p.StandardOutput.ReadToEnd().Trim();
p.WaitForExit();
return s;
}
}
Call Stamp.Write() from your build method before BuildPipeline.BuildPlayer, or from a build callback (IPreprocessBuildWithContext runs at the start of a build). Read it at runtime and show the hash on the about screen and in the first log line; the dirty flag marks builds made from uncommitted changes.
Running SteamCMD
From the builder folder, Valve's command is:
.\steamcmd.exe +login <account_name> <password> +run_app_build ..\scripts\app_build_1000.vdf +quit
The script path is relative to the builder folder. SteamCMD updates itself, logs in, splits files into roughly 1 MB chunks, uploads only the new ones, writes a manifest per depot and assigns a BuildID, so a nearly identical build uploads quickly.
Steam Guard and the login token
If login fails with "Account Login Denied", Steam Guard is blocking it. Find the code in the account's email, run set_steam_guard_code <code> in SteamCMD and log in again. SteamCMD then keeps a login token in config\config.vdf; Valve says to preserve that file between runs, and entering the password again issues a new token.
That file is a credential. If SteamCMD lives inside your project tree, add its folder to .gitignore before the first login, not after. The VDF scripts hold no secrets and can be committed.
Setting live on a branch
Uploading changes nothing for players. The build appears on the Builds page in App Admin (partner.steamgames.com/apps/builds) with your Desc. To release it, pick the branch in the "Set build live for branch..." dropdown, click "Preview Change", then "Set Build Live Now". Players on that branch get the update within a couple of minutes.
SetLive skips those clicks for a beta branch only. The default branch, the one customers are on, is set live by hand, and for a released game that needs an extra authorization through the Steam Mobile Authenticator or SMS. An empty SetLive is the safe default: a script can upload as often as it likes, and a person still makes the release decision.
Valve's updating guide suggests: upload, set the build live on a password-protected test branch (set the password first), test through the Steam client, then move the same build to default.
The same steps from CI
Every step above is a command. Unity builds from the command line with -batchmode, which suppresses dialogs, -executeMethod, which runs a static method from a script in an Editor folder, and -quit:
& "C:\Program Files\Unity\Hub\Editor\<version>\Editor\Unity.exe" -batchmode -nographics -quit `
-projectPath "C:\src\MyGame" -executeMethod BuildScript.BuildWindows -logFile -
-logFile - sends the log to the console. An unhandled exception makes Unity exit with code 1; so does EditorApplication.Exit(1), which is how to report a failed BuildPlayer. Then run the same steamcmd.exe line with the password from a CI secret and the config folder restored from the previous run, so the Steam Guard token is still valid; the first login on a fresh runner still needs a person with the code. With SetLive empty, the pipeline ends with a build waiting on the Builds page.
Where Ship It fits
Ship It is an editor tool from this publisher (39.99 USD, Unity 6.3 LTS and newer, in review on the Asset Store as of 2026-10-29) that does the steps above from a build profile. It writes the app and depot VDFs with the profile's output folder as content root, downloads SteamCMD if you have none, and runs the upload in a visible console where you type the password and the Steam Guard code yourself. It stores no credentials and writes a .gitignore into the SteamCMD folder so the login token never reaches your repository. Debug symbols and Unity's DoNotShip folders are left out of the depot and of the zip, and every build carries a build-info.json with the git hash and a dirty flag, readable at runtime through BuildInfo.Current. A branch is set live only when you name one.
It does not do achievements or stats, store page assets, or itch.io and Epic uploads. If you ship once a year, the manual route above is enough; if you ship every week, or also build servers and run CI, that is what it is for.
Sources
- https://partner.steamgames.com/doc/sdk/uploading
- https://partner.steamgames.com/doc/store/application/branches
- https://partner.steamgames.com/doc/store/application/builds
- https://partner.steamgames.com/doc/sdk/updating
- https://docs.unity.com/en-us/engine/6000.3/manual/platform-specific/windows/develop/player-il2cppscripting-backend
- https://docs.unity.com/en-us/engine/6000.3/manual/platform-specific/windows/standalone-binaries
- https://docs.unity.com/en-us/engine/6000.3/manual/building-and-publishing/streaming-assets
- https://docs.unity.com/en-us/engine/6000.3/manual/building-and-publishing/build-customize-build-pipeline/build-callbacks
- https://docs.unity.com/en-us/engine/6000.3/manual/unity-editor/command-line-arguments/editor
- https://docs.unity.com/en-us/engine/6000.3/manual/unity-editor/command-line-arguments/run-unity
- https://docs.unity3d.com/Packages/com.unity.burst@1.8/changelog/CHANGELOG.html