├── .gitignore ├── BuildUploader.Console ├── App.config ├── BuildConfiguration.cs ├── BuildDefinition.cs ├── BuildUploader.Console.csproj ├── Program.cs ├── Properties │ └── AssemblyInfo.cs ├── SteamSettings.cs ├── UnityCloudBuildSettings.cs └── packages.config ├── BuildUploader.sln ├── LICENSE ├── README.md ├── UnityCloudBuildSteamUploader ├── Setup-Project.ps1 ├── Steamworks_SDK │ ├── Publish-Build.bat │ └── scripts │ │ └── README.md ├── Upload-SteamContent.ps1 ├── configs │ └── README.md └── dist │ └── README.md ├── appveyor.yml ├── docs └── img │ └── CloudBuildMetadataHowTo.jpg └── img └── Slack Bot Icon.psd /.gitignore: -------------------------------------------------------------------------------- 1 | ## Custom stuff for this project 2 | # These will contain files downloaded by the SDK or 3 | # generated by the game developer 4 | UnityCloudBuildSteamUploader/Steamworks_SDK/content/* 5 | UnityCloudBuildSteamUploader/Steamworks_SDK/output/* 6 | UnityCloudBuildSteamUploader/Steamworks_SDK/builder/* 7 | UnityCloudBuildSteamUploader/Steamworks_SDK/builder_linux/* 8 | UnityCloudBuildSteamUploader/Steamworks_SDK/builder_osx/* 9 | 10 | # This is the program itself, it shouldn't be checked in. 11 | UnityCloudBuildSteamUploader/BuildUploader.Console.* 12 | UnityCloudBuildSteamUploader/Newtonsoft.* 13 | UnityCloudBuildSteamUploader/README.md 14 | 15 | # This will contain files downloaded from Unity Cloud build 16 | UnityCloudBuildSteamUploader/dist/ 17 | 18 | # These will be generated 19 | UnityCloudBuildSteamUploader/Steamworks_SDK/scripts/*.vdf 20 | UnityCloudBuildSteamUploader/configs 21 | 22 | ## Ignore Visual Studio temporary files, build results, and 23 | ## files generated by popular Visual Studio add-ons. 24 | ## 25 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 26 | 27 | # User-specific files 28 | *.suo 29 | *.user 30 | *.userosscache 31 | *.sln.docstates 32 | 33 | # User-specific files (MonoDevelop/Xamarin Studio) 34 | *.userprefs 35 | 36 | # Build results 37 | [Dd]ebug/ 38 | [Dd]ebugPublic/ 39 | [Rr]elease/ 40 | [Rr]eleases/ 41 | x64/ 42 | x86/ 43 | bld/ 44 | [Bb]in/ 45 | [Oo]bj/ 46 | [Ll]og/ 47 | 48 | # Visual Studio 2015/2017 cache/options directory 49 | .vs/ 50 | # Uncomment if you have tasks that create the project's static files in wwwroot 51 | #wwwroot/ 52 | 53 | # MSTest test Results 54 | [Tt]est[Rr]esult*/ 55 | [Bb]uild[Ll]og.* 56 | 57 | # NUNIT 58 | *.VisualState.xml 59 | TestResult.xml 60 | 61 | # Build Results of an ATL Project 62 | [Dd]ebugPS/ 63 | [Rr]eleasePS/ 64 | dlldata.c 65 | 66 | # Benchmark Results 67 | BenchmarkDotNet.Artifacts/ 68 | 69 | # .NET Core 70 | project.lock.json 71 | project.fragment.lock.json 72 | artifacts/ 73 | **/Properties/launchSettings.json 74 | 75 | *_i.c 76 | *_p.c 77 | *_i.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.pch 82 | *.pdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *.log 93 | *.vspscc 94 | *.vssscc 95 | .builds 96 | *.pidb 97 | *.svclog 98 | *.scc 99 | 100 | # Chutzpah Test files 101 | _Chutzpah* 102 | 103 | # Visual C++ cache files 104 | ipch/ 105 | *.aps 106 | *.ncb 107 | *.opendb 108 | *.opensdf 109 | *.sdf 110 | *.cachefile 111 | *.VC.db 112 | *.VC.VC.opendb 113 | 114 | # Visual Studio profiler 115 | *.psess 116 | *.vsp 117 | *.vspx 118 | *.sap 119 | 120 | # Visual Studio Trace Files 121 | *.e2e 122 | 123 | # TFS 2012 Local Workspace 124 | $tf/ 125 | 126 | # Guidance Automation Toolkit 127 | *.gpState 128 | 129 | # ReSharper is a .NET coding add-in 130 | _ReSharper*/ 131 | *.[Rr]e[Ss]harper 132 | *.DotSettings.user 133 | 134 | # JustCode is a .NET coding add-in 135 | .JustCode 136 | 137 | # TeamCity is a build add-in 138 | _TeamCity* 139 | 140 | # DotCover is a Code Coverage Tool 141 | *.dotCover 142 | 143 | # AxoCover is a Code Coverage Tool 144 | .axoCover/* 145 | !.axoCover/settings.json 146 | 147 | # Visual Studio code coverage results 148 | *.coverage 149 | *.coveragexml 150 | 151 | # NCrunch 152 | _NCrunch_* 153 | .*crunch*.local.xml 154 | nCrunchTemp_* 155 | 156 | # MightyMoose 157 | *.mm.* 158 | AutoTest.Net/ 159 | 160 | # Web workbench (sass) 161 | .sass-cache/ 162 | 163 | # Installshield output folder 164 | [Ee]xpress/ 165 | 166 | # DocProject is a documentation generator add-in 167 | DocProject/buildhelp/ 168 | DocProject/Help/*.HxT 169 | DocProject/Help/*.HxC 170 | DocProject/Help/*.hhc 171 | DocProject/Help/*.hhk 172 | DocProject/Help/*.hhp 173 | DocProject/Help/Html2 174 | DocProject/Help/html 175 | 176 | # Click-Once directory 177 | publish/ 178 | 179 | # Publish Web Output 180 | *.[Pp]ublish.xml 181 | *.azurePubxml 182 | # Note: Comment the next line if you want to checkin your web deploy settings, 183 | # but database connection strings (with potential passwords) will be unencrypted 184 | *.pubxml 185 | *.publishproj 186 | 187 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 188 | # checkin your Azure Web App publish settings, but sensitive information contained 189 | # in these scripts will be unencrypted 190 | PublishScripts/ 191 | 192 | # NuGet Packages 193 | *.nupkg 194 | # The packages folder can be ignored because of Package Restore 195 | **/[Pp]ackages/* 196 | # except build/, which is used as an MSBuild target. 197 | !**/[Pp]ackages/build/ 198 | # Uncomment if necessary however generally it will be regenerated when needed 199 | #!**/[Pp]ackages/repositories.config 200 | # NuGet v3's project.json files produces more ignorable files 201 | *.nuget.props 202 | *.nuget.targets 203 | 204 | # Microsoft Azure Build Output 205 | csx/ 206 | *.build.csdef 207 | 208 | # Microsoft Azure Emulator 209 | ecf/ 210 | rcf/ 211 | 212 | # Windows Store app package directories and files 213 | AppPackages/ 214 | BundleArtifacts/ 215 | Package.StoreAssociation.xml 216 | _pkginfo.txt 217 | *.appx 218 | 219 | # Visual Studio cache files 220 | # files ending in .cache can be ignored 221 | *.[Cc]ache 222 | # but keep track of directories ending in .cache 223 | !*.[Cc]ache/ 224 | 225 | # Others 226 | ClientBin/ 227 | ~$* 228 | *~ 229 | *.dbmdl 230 | *.dbproj.schemaview 231 | *.jfm 232 | *.pfx 233 | *.publishsettings 234 | orleans.codegen.cs 235 | 236 | # Since there are multiple workflows, uncomment next line to ignore bower_components 237 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 238 | #bower_components/ 239 | 240 | # RIA/Silverlight projects 241 | Generated_Code/ 242 | 243 | # Backup & report files from converting an old project file 244 | # to a newer Visual Studio version. Backup files are not needed, 245 | # because we have git ;-) 246 | _UpgradeReport_Files/ 247 | Backup*/ 248 | UpgradeLog*.XML 249 | UpgradeLog*.htm 250 | 251 | # SQL Server files 252 | *.mdf 253 | *.ldf 254 | *.ndf 255 | 256 | # Business Intelligence projects 257 | *.rdl.data 258 | *.bim.layout 259 | *.bim_*.settings 260 | 261 | # Microsoft Fakes 262 | FakesAssemblies/ 263 | 264 | # GhostDoc plugin setting file 265 | *.GhostDoc.xml 266 | 267 | # Node.js Tools for Visual Studio 268 | .ntvs_analysis.dat 269 | node_modules/ 270 | 271 | # TypeScript v1 declaration files 272 | typings/ 273 | 274 | # Visual Studio 6 build log 275 | *.plg 276 | 277 | # Visual Studio 6 workspace options file 278 | *.opt 279 | 280 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 281 | *.vbw 282 | 283 | # Visual Studio LightSwitch build output 284 | **/*.HTMLClient/GeneratedArtifacts 285 | **/*.DesktopClient/GeneratedArtifacts 286 | **/*.DesktopClient/ModelManifest.xml 287 | **/*.Server/GeneratedArtifacts 288 | **/*.Server/ModelManifest.xml 289 | _Pvt_Extensions 290 | 291 | # Paket dependency manager 292 | .paket/paket.exe 293 | paket-files/ 294 | 295 | # FAKE - F# Make 296 | .fake/ 297 | 298 | # JetBrains Rider 299 | .idea/ 300 | *.sln.iml 301 | 302 | # CodeRush 303 | .cr/ 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ -------------------------------------------------------------------------------- /BuildUploader.Console/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /BuildUploader.Console/BuildConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace BuildUploader.Console 4 | { 5 | internal class BuildConfiguration 6 | { 7 | [JsonProperty("unity")] 8 | public UnityCloudBuildSettings UnitySettings { get; internal set; } 9 | 10 | [JsonProperty("steam")] 11 | public SteamSettings SteamSettings { get; internal set; } 12 | } 13 | } -------------------------------------------------------------------------------- /BuildUploader.Console/BuildDefinition.cs: -------------------------------------------------------------------------------- 1 | namespace BuildUploader.Console 2 | { 3 | internal class BuildDefinition 4 | { 5 | public string BuildTarget; 6 | 7 | public int BuildNumber; 8 | 9 | public string FileName; 10 | 11 | public string DownloadUrl; 12 | 13 | public override string ToString() 14 | { 15 | return string.Format("Build({0})", this.FileName); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /BuildUploader.Console/BuildUploader.Console.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {E263FC4E-A1C8-444A-8531-5DC231C0029F} 8 | Exe 9 | BuildUploader.Console 10 | BuildUploader.Console 11 | v4.8 12 | 512 13 | true 14 | 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | ..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | xcopy /Y /R "$(ProjectDir)$(OutDir)*" "$(SolutionDir)UnityCloudBuildSteamUploader" 64 | xcopy /Y /R "$(SolutionDir)README.md" "$(SolutionDir)UnityCloudBuildSteamUploader" 65 | 66 | -------------------------------------------------------------------------------- /BuildUploader.Console/Program.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Configuration; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.IO.Compression; 7 | using System.Net; 8 | using System.Net.Http; 9 | using System.Net.Http.Headers; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | using System.Timers; 13 | 14 | namespace BuildUploader.Console 15 | { 16 | class Program 17 | { 18 | private static Timer timer; 19 | private static string pollingFrequencyRaw; 20 | private static int pollingFrequency; 21 | 22 | static void Main(string[] args) 23 | { 24 | Trace.Listeners.Add(new ConsoleTraceListener()); 25 | 26 | pollingFrequencyRaw = ConfigurationSettings.AppSettings["POLLING_FREQUENCY"]; 27 | pollingFrequency = int.Parse(pollingFrequencyRaw) * 1000 * 60; 28 | 29 | ScanForNewBuilds(null, null); 30 | 31 | timer = new Timer(); 32 | timer.Interval = pollingFrequency; 33 | timer.Elapsed += ScanForNewBuilds; 34 | timer.Start(); 35 | 36 | System.Console.Write("Press any key to exit... "); 37 | System.Console.ReadKey(); 38 | } 39 | 40 | private static void ScanForNewBuilds(object sender, ElapsedEventArgs e) 41 | { 42 | Trace.TraceInformation("Scanning for new Unity Cloud Builds at {0:MM/dd/yy H:mm}", DateTime.Now); 43 | System.Console.WriteLine(); 44 | 45 | foreach (var configFile in Directory.EnumerateFiles("configs")) 46 | { 47 | if (!configFile.EndsWith("json")) 48 | { 49 | continue; 50 | } 51 | 52 | Trace.TraceInformation("Processing config file: {0}", Path.GetFileNameWithoutExtension(configFile)); 53 | 54 | var buildConfig = JsonConvert.DeserializeObject(File.ReadAllText(configFile)); 55 | var downloadBuildDataTask = Task.Run(() => DownloadUnityCloudBuildMetadata(buildConfig.UnitySettings)); 56 | downloadBuildDataTask.Wait(); 57 | var latestBuild = downloadBuildDataTask.Result; 58 | 59 | if (latestBuild != null) 60 | { 61 | var successfullyDownloadedBuild = DownloadUnityCloudBuild(buildConfig.SteamSettings, latestBuild); 62 | if (successfullyDownloadedBuild) 63 | { 64 | bool success = UploadBuildToSteamworks(buildConfig.SteamSettings, latestBuild); 65 | TryNotifySlack(buildConfig.SteamSettings, latestBuild, success); 66 | } 67 | } 68 | 69 | Trace.TraceInformation("Finished processing config file: {0}", Path.GetFileNameWithoutExtension(configFile)); 70 | System.Console.WriteLine(); 71 | } 72 | 73 | Trace.TraceInformation("Finished scanning for new Unity Cloud Builds"); 74 | Trace.TraceInformation( 75 | "Checking for new builds in {0} minutes at {1:MM/dd/yy H:mm}", 76 | pollingFrequencyRaw, 77 | DateTime.Now + TimeSpan.FromMilliseconds(pollingFrequency)); 78 | } 79 | 80 | private static void TryNotifySlack(SteamSettings steamSettings, BuildDefinition latestBuild, bool success) 81 | { 82 | var slackUrl = ConfigurationSettings.AppSettings["SLACK_NOTIFICATION_URL"]; 83 | if (!string.IsNullOrEmpty(slackUrl)) 84 | { 85 | Trace.TraceInformation("Sending Slack notification"); 86 | string payload; 87 | if (success) 88 | { 89 | payload = string.Format( 90 | "{0} build `{1} {2}` has been uploaded to the Steam `{3}`.", 91 | steamSettings.DisplayName, 92 | latestBuild.BuildTarget, 93 | latestBuild.BuildNumber, 94 | steamSettings.BranchName ?? "default"); 95 | } 96 | else 97 | { 98 | payload = string.Format( 99 | "Failed to upload {0} build `{1} {2}` to Steam `{3}`.", 100 | steamSettings.DisplayName, 101 | latestBuild.BuildTarget, 102 | latestBuild.BuildNumber, 103 | steamSettings.BranchName ?? "default"); 104 | } 105 | 106 | var message = @"{""text"": """ + payload + @"""}"; 107 | 108 | using (var client = new HttpClient()) 109 | { 110 | HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, slackUrl); 111 | request.Content = new StringContent(message, Encoding.UTF8, "application/json"); 112 | var task = client.SendAsync(request); 113 | task.Wait(); 114 | } 115 | } 116 | } 117 | 118 | private static bool UploadBuildToSteamworks(SteamSettings steamSettings, BuildDefinition latestBuild) 119 | { 120 | var buildDescription = string.Format("{0} {1}", latestBuild.BuildTarget, latestBuild.BuildNumber); 121 | var steamworksDir = ConfigurationSettings.AppSettings["STEAMWORKS_DIRECTORY"]; 122 | Trace.TraceInformation("Invoking Steamworks SDK to upload build"); 123 | string command = string.Format( 124 | @"{0}\Publish-Build.bat {1} ""{2}"" {3} {4} ""{5}"" ""{6}""", 125 | steamworksDir, 126 | steamSettings.Username, 127 | steamSettings.Password, 128 | steamSettings.AppId, 129 | steamSettings.AppScript, 130 | Environment.CurrentDirectory + "\\" + steamSettings.ExecutablePath, 131 | buildDescription); 132 | 133 | int exitCode; 134 | ProcessStartInfo processInfo; 135 | Process process; 136 | 137 | processInfo = new ProcessStartInfo("cmd.exe", "/c " + command); 138 | processInfo.WorkingDirectory = Environment.CurrentDirectory; 139 | processInfo.CreateNoWindow = true; 140 | processInfo.UseShellExecute = false; 141 | // *** Redirect the output *** 142 | processInfo.RedirectStandardError = true; 143 | processInfo.RedirectStandardOutput = true; 144 | 145 | process = Process.Start(processInfo); 146 | process.WaitForExit(); 147 | 148 | // *** Read the streams *** 149 | // Warning: This approach can lead to deadlocks, see Edit #2 150 | string output = process.StandardOutput.ReadToEnd(); 151 | string error = process.StandardError.ReadToEnd(); 152 | 153 | exitCode = process.ExitCode; 154 | 155 | Trace.TraceInformation(output); 156 | if (exitCode == 0) 157 | { 158 | Trace.TraceInformation("Steaworks SDK finished successfully"); 159 | } 160 | else 161 | { 162 | Trace.TraceError(error); 163 | Trace.TraceError("Steaworks SDK failed"); 164 | } 165 | 166 | process.Close(); 167 | 168 | return exitCode == 0; 169 | } 170 | 171 | private static bool DownloadUnityCloudBuild(SteamSettings steamSettings, BuildDefinition latestBuild) 172 | { 173 | bool success = true; 174 | Trace.TraceInformation("Checking whether latest build has already been processed"); 175 | var downloadDir = ConfigurationSettings.AppSettings["DOWNLOAD_DIRECTORY"]; 176 | var filePath = downloadDir + "/" + latestBuild.FileName; 177 | if (File.Exists(filePath)) 178 | { 179 | Trace.TraceInformation("Build already processed"); 180 | success = false; 181 | } 182 | else 183 | { 184 | Trace.TraceInformation("Downloading new build"); 185 | 186 | using (var webClient = new WebClient()) 187 | { 188 | webClient.DownloadFile(new Uri(latestBuild.DownloadUrl), filePath); 189 | } 190 | 191 | Trace.TraceInformation("Downloaded new build"); 192 | 193 | if (Directory.Exists(steamSettings.ContentDir)) 194 | { 195 | Trace.TraceInformation("Deleting existing Steamworks content"); 196 | Directory.Delete(steamSettings.ContentDir, true); 197 | } 198 | 199 | Trace.TraceInformation("Unzipping build"); 200 | ZipFile.ExtractToDirectory(filePath, steamSettings.ContentDir); 201 | Trace.TraceInformation("Unzipped build"); 202 | success = true; 203 | } 204 | 205 | return success; 206 | } 207 | 208 | public static async Task DownloadUnityCloudBuildMetadata(UnityCloudBuildSettings cloudBuildSettings) 209 | { 210 | StringBuilder urlBuilder = new StringBuilder("https://build-api.cloud.unity3d.com/api/v1"); 211 | urlBuilder.Append("/orgs/"); 212 | urlBuilder.Append(cloudBuildSettings.OrganizationID); 213 | urlBuilder.Append("/projects/"); 214 | urlBuilder.Append(cloudBuildSettings.ProjectName); 215 | urlBuilder.Append("/buildtargets/"); 216 | urlBuilder.Append(cloudBuildSettings.TargetId); 217 | urlBuilder.Append("/builds?buildStatus=success"); 218 | 219 | var request = new HttpRequestMessage(HttpMethod.Get, urlBuilder.ToString()); 220 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 221 | request.Headers.Authorization = new AuthenticationHeaderValue("Basic", cloudBuildSettings.APIKey); 222 | 223 | var client = new HttpClient(); 224 | 225 | Trace.TraceInformation("Downloading cloud build information."); 226 | BuildDefinition result; 227 | var response = await client.SendAsync(request); 228 | if (!response.IsSuccessStatusCode) 229 | { 230 | Trace.TraceError("Failed to download cloud build information: " + response.StatusCode); 231 | result = null; 232 | } 233 | else 234 | { 235 | var json = await response.Content.ReadAsStringAsync(); 236 | Trace.TraceInformation("Parsing cloud build information."); 237 | dynamic successfulBuilds = JsonConvert.DeserializeObject(json); 238 | 239 | int latestBuildNumber = 0; 240 | BuildDefinition latestBuild = null; 241 | foreach (var build in successfulBuilds) 242 | { 243 | int buildNumber = build.build; 244 | if (latestBuild == null || latestBuildNumber < buildNumber) 245 | { 246 | latestBuildNumber = buildNumber; 247 | latestBuild = new BuildDefinition() 248 | { 249 | BuildTarget = build.buildtargetid, 250 | BuildNumber = build.build, 251 | DownloadUrl = build.links.download_primary.href, 252 | FileName = build.build + "_" + cloudBuildSettings.ProjectName + "_" + build.buildtargetid + '.' + build.links.download_primary.meta.type, 253 | }; 254 | } 255 | } 256 | 257 | Trace.TraceInformation("Found build: {0}.", latestBuildNumber); 258 | result = latestBuild; 259 | } 260 | 261 | return result; 262 | } 263 | } 264 | } -------------------------------------------------------------------------------- /BuildUploader.Console/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("ConsoleApp1")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("ConsoleApp1")] 13 | [assembly: AssemblyCopyright("Copyright © 2017")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("e263fc4e-a1c8-444a-8531-5dc231c0029f")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /BuildUploader.Console/SteamSettings.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace BuildUploader.Console 4 | { 5 | internal class SteamSettings 6 | { 7 | [JsonProperty("app_id")] 8 | public string AppId { get; internal set; } 9 | 10 | [JsonProperty("display_name")] 11 | public string DisplayName { get; internal set; } 12 | 13 | [JsonProperty("branch_name")] 14 | public string BranchName { get; internal set; } 15 | 16 | [JsonProperty("username")] 17 | public string Username { get; internal set; } 18 | 19 | [JsonProperty("password")] 20 | public string Password { get; internal set; } 21 | 22 | [JsonProperty("app_script")] 23 | public string AppScript { get; internal set; } 24 | 25 | [JsonProperty("content_dir")] 26 | public string ContentDir { get; internal set; } 27 | 28 | [JsonProperty("exe_path")] 29 | public string ExecutablePath { get; internal set; } 30 | } 31 | } -------------------------------------------------------------------------------- /BuildUploader.Console/UnityCloudBuildSettings.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace BuildUploader.Console 4 | { 5 | internal class UnityCloudBuildSettings 6 | { 7 | [JsonProperty("org_id")] 8 | public string OrganizationID { get; internal set; } 9 | 10 | [JsonProperty("project")] 11 | public string ProjectName { get; internal set; } 12 | 13 | [JsonProperty("target")] 14 | public string TargetId { get; internal set; } 15 | 16 | [JsonProperty("api_key")] 17 | public string APIKey { get; internal set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /BuildUploader.Console/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /BuildUploader.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27004.2008 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BuildUploader.Console", "BuildUploader.Console\BuildUploader.Console.csproj", "{E263FC4E-A1C8-444A-8531-5DC231C0029F}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{FA90DF51-3E07-40BB-8A45-B9C57E6A42C3}" 9 | ProjectSection(SolutionItems) = preProject 10 | .gitignore = .gitignore 11 | UnityCloudBuildSteamUploader\Steamworks_SDK\Publish-Build.bat = UnityCloudBuildSteamUploader\Steamworks_SDK\Publish-Build.bat 12 | README.md = README.md 13 | UnityCloudBuildSteamUploader\Setup-Project.ps1 = UnityCloudBuildSteamUploader\Setup-Project.ps1 14 | UnityCloudBuildSteamUploader\Upload-SteamContent.ps1 = UnityCloudBuildSteamUploader\Upload-SteamContent.ps1 15 | EndProjectSection 16 | EndProject 17 | Global 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|Any CPU = Debug|Any CPU 20 | Release|Any CPU = Release|Any CPU 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {E263FC4E-A1C8-444A-8531-5DC231C0029F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {E263FC4E-A1C8-444A-8531-5DC231C0029F}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {E263FC4E-A1C8-444A-8531-5DC231C0029F}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {E263FC4E-A1C8-444A-8531-5DC231C0029F}.Release|Any CPU.Build.0 = Release|Any CPU 27 | EndGlobalSection 28 | GlobalSection(SolutionProperties) = preSolution 29 | HideSolutionNode = FALSE 30 | EndGlobalSection 31 | GlobalSection(ExtensibilityGlobals) = postSolution 32 | SolutionGuid = {BF3055E5-B63F-4D7F-96A4-2E4EDE1444A5} 33 | EndGlobalSection 34 | EndGlobal 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alex Schearer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build status](https://ci.appveyor.com/api/projects/status/6hp8elqmalmko9b7?svg=true)](https://ci.appveyor.com/project/aschearer/unitycloudbuildsteamuploader) 2 | 3 | # Unity Cloud Build to Steam Uploader 4 | I really enjoy using Unity's Cloud Build to create new versions of my game. It lets me focus on adding more features while a build cooks in the background. But uploading the new build still requires manual work on my part. I've got to find the build on Unity's website, download and unzip it, move it to the proper directory for upload, and execute Valve's Steamworks upload script. I just want to stay focused working on my game! 5 | 6 | The goal of this project is to automate the remaining manual steps. It does that by polling Unity Cloud Build for changes and uploading them automatically to Steam. 7 | 8 | ## Prerequisites 9 | * You're using Windows. 10 | * You have a Steam app registered and a depot set up. 11 | * You have Unity Cloud Build up and running. 12 | * You can execute PowerShell scripts. 13 | * You've downloaded the [Steamworks SDK][2] 14 | 15 | ### Getting Your Unity Cloud Build Information ### 16 | 17 | You can find your API Key here: 18 | 1. Open this url: https://dashboard.unity3d.com/develop/ 19 | 1. Select your game's project name 20 | 1. Within that project's dashboard page, navigate to: Settings > Cloud Build > API Settings > API Key 21 | 22 | To get the rest of the information, go to the summary for your most recent build. In the URL you will see the necessary pieces: 23 | 24 | ![Unity Cloud Build data example](docs/img/CloudBuildMetadataHowTo.jpg) 25 | 26 | ### Enable PowerShell Script Execution ### 27 | 28 | 1. Open a PowerShell terminal as administrator 29 | 1. Execute: `Set-ExecutionPolicy Unrestricted` 30 | 31 | ## Quick Start 32 | Download and unzip the latest build here: 33 | 34 | [UnityCloudBuildSteamUploader.zip][1] 35 | 36 | Copy the following folders to `$PROJECT\UnityCloudBuildSteamUploader\Steamworks_SDK`: 37 | 38 | 1. `$SDK\sdk\tools\ContentBuilder\builder` 39 | 1. `$SDK\sdk\tools\ContentBuilder\builder_linux` 40 | 1. `$SDK\sdk\tools\ContentBuilder\builder_osx` 41 | 42 | Navigate to the project folder, open PowerShell, and run: 43 | 44 | Setup-Project.ps1 45 | 46 | Follow the prompts to configure a project. You should run this script once for each project you want to set up. This script creates config files under `configs/` which you can safely edit. 47 | 48 | Next we must disable Steam Guard. To do so run: 49 | 50 | Upload-SteamContent.ps1 51 | 52 | And provide it with the file name of one of the config files generated in the previous step. This will prompt you to enter the Steam Guard code. You can then cancel the process, or let it finish (it will upload a blank build). 53 | 54 | Note: You can use `Upload-SteamContent.ps1` as a means to manually upload builds to Steam. If you don't want to use Unity Cloud Build, just place your game's files under the corresponding content folder (defined in the game's project settings generated in the previous step) and run this script. 55 | 56 | Finally run: 57 | 58 | BuildUploader.Console.exe 59 | 60 | This program should be left running. It will check each project for new builds and upload them to Steam automatically. If the terminal is closed for whatever reason simply restart the program. 61 | 62 | ## Contributing 63 | The following is intended for those who would like to change the tool. If you simply want to run the tool refer to the Quick Start section above. 64 | 65 | ### Installation 66 | 67 | * Open `BuildUploader.sln` in Visual Studio. 68 | * Restore Nuget packages. 69 | * Build the solution. 70 | 71 | The generated exe and dependencies will be automatically copied to `UnityCloudBuildSteamUploader`. Use that directory to test changes. It also serves as the basis for creating new release. 72 | 73 | ### Original Implementation and Reference 74 | Credit to Niklas Borglund for his guide detailing how to use Node.js and Gulp to scan for, download, and publish builds from Unity Cloud Build. 75 | 76 | https://www.lavapotion.com/blog/2017/6/8/connecting-unity-cloud-build-and-hockeyapp-the-slightly-hackish-way 77 | 78 | [1]: https://github.com/aschearer/UnityCloudBuildSteamUploader/releases/latest 79 | [2]: https://partner.steamgames.com/downloads/steamworks_sdk.zip 80 | -------------------------------------------------------------------------------- /UnityCloudBuildSteamUploader/Setup-Project.ps1: -------------------------------------------------------------------------------- 1 | # Sets up the Steamworks SDK so it's ready to be run. 2 | $displayName = Read-Host 'Project Name' 3 | $appId = Read-Host 'Steam App Id' 4 | $depotId = Read-Host 'Steam Depot Id' 5 | $buildDescription = Read-Host 'Steam Build Description (Internal Only)' 6 | $buildBranch = Read-Host 'Steam Build Branch (Blank uploads to Default)' 7 | $exeName = Read-Host 'Executable Name' 8 | $steamUsername = Read-Host 'Steam username' 9 | $steamPassword = Read-Host 'Steam password' -AsSecureString 10 | 11 | # Prompts for information needed to scan Unity Cloud Build and upload to Steamworks. 12 | # Stores the necessary information in config.json. 13 | 14 | $cloudBuildAPIKey = Read-Host 'Unity Cloud Build API Key' 15 | $cloudBuildOrgId = Read-Host 'Unity Cloud Build Organization Name' 16 | $cloudBuildProject = Read-Host 'Unity Cloud Build Project Name' 17 | $cloudBuildTargetName = Read-Host 'Unity Cloud Build Target Name' 18 | 19 | $steamworksDir = 'Steamworks_SDK' 20 | $scriptsDir = "${steamworksDir}\scripts" 21 | $projectPrefix = "${cloudBuildProject}_${cloudBuildTargetName}" 22 | $appScriptName = "${projectPrefix}_app_build_${appId}.vdf" 23 | $depotScriptName = "${projectPrefix}_depot_build_depot_build_${depotId}" 24 | $contentDir = "${steamworksDir}\content\${projectPrefix}_content" 25 | 26 | Write-Host "Creating App Script" 27 | Set-Content "$scriptsDir/$appScriptName" @" 28 | "appbuild" 29 | { 30 | "appid" "$appId" 31 | "desc" "$buildDescription" // description for this build 32 | "buildoutput" "..\output\" // build output folder for .log, .csm & .csd files, relative to location of this file 33 | "contentroot" "..\content\" // root content folder, relative to location of this file 34 | "setlive" "$buildBranch" // branch to set live after successful build, non if empty 35 | "preview" "0" // to enable preview builds 36 | "local" "" // set to flie path of local content server 37 | 38 | "depots" 39 | { 40 | "$depotId" "$depotScriptName.vdf" 41 | } 42 | } 43 | "@ 44 | 45 | Write-Host "Creating Depot Script" 46 | Set-Content "$scriptsDir\$depotScriptName.vdf" @" 47 | "DepotBuildConfig" 48 | { 49 | // Set your assigned depot ID here 50 | "DepotID" "$depotId" 51 | 52 | // include all files recursivley 53 | "FileMapping" 54 | { 55 | // This can be a full path, or a path relative to ContentRoot 56 | "LocalPath" ".\${projectPrefix}_content\*" 57 | 58 | // This is a path relative to the install folder of your game 59 | "DepotPath" "." 60 | 61 | // If LocalPath contains wildcards, setting this means that all 62 | // matching files within subdirectories of LocalPath will also 63 | // be included. 64 | "recursive" "1" 65 | } 66 | 67 | // but exclude all symbol files 68 | // This can be a full path, or a path relative to ContentRoot 69 | "FileExclusion" "*.pdb" 70 | } 71 | "@ 72 | 73 | Write-Host "Creating Steamworks Content Directory" 74 | New-Item -ItemType Directory -Path $contentDir 75 | 76 | $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($steamPassword) 77 | $plainPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) 78 | 79 | # We must escape the content dir otherwise the JSON will not be valid 80 | $in = '\' 81 | $out = '\\' 82 | $jsonContentDir = $contentDir -replace [RegEx]::Escape($in), "$out" 83 | Set-Content "configs/${projectPrefix}_config.json" @" 84 | { 85 | "unity": { 86 | "api_key": "$cloudBuildAPIKey", 87 | "org_id": "$cloudBuildOrgId", 88 | "project": "$cloudBuildProject", 89 | "target": "$cloudBuildTargetName" 90 | }, 91 | "steam": { 92 | "app_id": "$appId", 93 | "display_name": "$displayName", 94 | "branch_name": "$buildBranch", 95 | "username": "$steamUsername", 96 | "password": "$plainPassword", 97 | "app_script": "$appScriptName", 98 | "content_dir": "$jsonContentDir", 99 | "exe_path": "$jsonContentDir\\$exeName" 100 | } 101 | } 102 | "@ 103 | Write-Host "Configuration information written to config.json" 104 | Write-Host "Project setup is complete." -------------------------------------------------------------------------------- /UnityCloudBuildSteamUploader/Steamworks_SDK/Publish-Build.bat: -------------------------------------------------------------------------------- 1 | echo off 2 | set username=%1 3 | set password=%2 4 | set appId=%3 5 | set appScript=%4 6 | set appExe=%5 7 | set buildDescription=%6 8 | "%~dp0\builder\steamcmd" +login %username% %password% %appId% "%appExe%" "%appExe%" +run_app_build -desc %buildDescription% ..\scripts\%appScript% +quit -------------------------------------------------------------------------------- /UnityCloudBuildSteamUploader/Steamworks_SDK/scripts/README.md: -------------------------------------------------------------------------------- 1 | This file primarily exists so that git will include this directory. 2 | 3 | It also exists to tell you that the purpose of this directory is to keep scripts needed by Steamworks in order to upload builds to Steam. -------------------------------------------------------------------------------- /UnityCloudBuildSteamUploader/Upload-SteamContent.ps1: -------------------------------------------------------------------------------- 1 | # This will run the publish script manually so you can confirm it 2 | # works and fix any issues with Steam Guard. 3 | 4 | $configFile = Read-Host 'Config File Name' 5 | 6 | $pwd = $PSScriptRoot 7 | 8 | $config = Get-Content "configs/$configFile" | ConvertFrom-Json 9 | 10 | $steamUsername = $config.steam.username 11 | $steamPassword = $config.steam.password 12 | $steamAppId = $config.steam.app_id 13 | $steamAppScript = $config.steam.app_script 14 | $steamExe = $pwd + "\" + $config.steam.exe_path 15 | $buildDescription = "" 16 | 17 | 18 | # This crazy thing is to make sure ^ and other special characters get through 19 | $passArg = @" 20 | "$steamPassword" 21 | "@ 22 | 23 | Write-Host "Invoking Steamworks SDK" 24 | 25 | & ".\Steamworks_SDK\Publish-Build.bat" $steamUsername $passArg $steamAppId $steamAppScript $steamExe $buildDescription -------------------------------------------------------------------------------- /UnityCloudBuildSteamUploader/configs/README.md: -------------------------------------------------------------------------------- 1 | This file primarily exists so that git will include this directory. 2 | 3 | It also exists to tell you that the purpose of this directory is to keep your project configuration files. -------------------------------------------------------------------------------- /UnityCloudBuildSteamUploader/dist/README.md: -------------------------------------------------------------------------------- 1 | This file primarily exists so that git will include this directory. 2 | 3 | It also exists to tell you that the purpose of this directory is to track builds downloaded from Unity Cloud Build. -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.0.{build} 2 | image: Visual Studio 2022 3 | before_build: 4 | - ps: nuget restore BuildUploader.sln 5 | build: 6 | verbosity: minimal 7 | after_build: 8 | - cmd: 7z a UnityCloudBuildSteamUploader.zip "%APPVEYOR_BUILD_FOLDER%\UnityCloudBuildSteamUploader\*" 9 | artifacts: 10 | - path: UnityCloudBuildSteamUploader.zip 11 | name: UnityCloudBuildSteamUploader.zip -------------------------------------------------------------------------------- /docs/img/CloudBuildMetadataHowTo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aschearer/UnityCloudBuildSteamUploader/991d18e5eeb1a50b8aaa9835e6b0004d9329382b/docs/img/CloudBuildMetadataHowTo.jpg -------------------------------------------------------------------------------- /img/Slack Bot Icon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aschearer/UnityCloudBuildSteamUploader/991d18e5eeb1a50b8aaa9835e6b0004d9329382b/img/Slack Bot Icon.psd --------------------------------------------------------------------------------