├── .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 | [](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 | 
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
--------------------------------------------------------------------------------