401 строка
15 KiB
C#
401 строка
15 KiB
C#
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// Licensed under the MIT License.
|
|
|
|
#tool nuget:?package=XamarinComponent
|
|
#addin nuget:?package=Cake.Xamarin
|
|
#addin nuget:?package=Cake.FileHelpers&version=3.0.0
|
|
#addin nuget:?package=Newtonsoft.Json
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Net;
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
// Task Target for build
|
|
var Target = Argument("target", Argument("t", "Default"));
|
|
|
|
string ArchiveDirectory = "archives";
|
|
bool IsMandatory = false;
|
|
string DistributionGroup = "Private Release Script Group";
|
|
string Token = EnvironmentVariable("APP_CENTER_API_TOKEN");
|
|
string BaseUrl = "https://api.appcenter.ms";
|
|
ApplicationInfo CurrentApp = null;
|
|
|
|
public enum Environment
|
|
{
|
|
Int,
|
|
Prod
|
|
}
|
|
|
|
public enum Platform
|
|
{
|
|
iOS,
|
|
Android,
|
|
UWP
|
|
}
|
|
|
|
public class ApplicationInfo
|
|
{
|
|
public static ICakeContext Context;
|
|
public static string OutputDirectory;
|
|
public Environment AppEnvironment { get; }
|
|
public Platform AppPlatform { get; }
|
|
public string AppOwner { get; }
|
|
public string AppId { get; }
|
|
public string AppPath
|
|
{
|
|
get
|
|
{
|
|
return OutputDirectory + "/" + AppId + "." + _appExtension;
|
|
}
|
|
}
|
|
public string ProjectPath
|
|
{
|
|
get
|
|
{
|
|
if (_projectPath == null)
|
|
{
|
|
_projectPath = Context.GetFiles("**/" + _projectFile).Single().ToString();
|
|
}
|
|
return _projectPath;
|
|
}
|
|
}
|
|
|
|
public string ProjectDirectory
|
|
{
|
|
get
|
|
{
|
|
return System.IO.Path.GetDirectoryName(ProjectPath);
|
|
}
|
|
}
|
|
|
|
private string _appExtension = null;
|
|
private string _projectPath = null;
|
|
private string _projectFile = null;
|
|
public ApplicationInfo(Environment environment, Platform platform, string appOwner, string appId, string projectFile, string appExtension)
|
|
{
|
|
AppOwner = appOwner;
|
|
AppId = appId;
|
|
AppEnvironment = environment;
|
|
AppPlatform = platform;
|
|
_projectFile = projectFile;
|
|
_appExtension = appExtension;
|
|
}
|
|
}
|
|
|
|
ApplicationInfo.Context = Context;
|
|
ApplicationInfo.OutputDirectory = ArchiveDirectory;
|
|
IList<ApplicationInfo> Applications = new List<ApplicationInfo>
|
|
{
|
|
new ApplicationInfo(Environment.Prod, Platform.iOS, "appcenter", "xamarin-demo-ios", "Contoso.Forms.Demo.iOS.csproj", "ipa"),
|
|
new ApplicationInfo(Environment.Prod, Platform.Android, "appcenter", "xamarin-demo-android", "Contoso.Forms.Demo.Droid.csproj", "apk"),
|
|
new ApplicationInfo(Environment.Prod, Platform.UWP, "appcenter-sdk", "UWP-Forms-Puppet", "Contoso.Forms.Demo.UWP.csproj", ""),
|
|
new ApplicationInfo(Environment.Int, Platform.iOS, "appcenter-sdk", "xamarin-puppet-ios", "Contoso.Forms.Puppet.iOS.csproj", "ipa"),
|
|
new ApplicationInfo(Environment.Int, Platform.Android, "appcenter-sdk", "xamarin-puppet-android-03", "Contoso.Forms.Puppet.Droid.csproj", "apk"),
|
|
new ApplicationInfo(Environment.Int, Platform.UWP, "appcenter-sdk", "xamarin-forms-puppet-uwp-2", "Contoso.Forms.Puppet.UWP.csproj", "")
|
|
};
|
|
|
|
Setup(context =>
|
|
{
|
|
// Arguments:
|
|
// -environment (-e): App "environment" ("prod" or "int") -- Default is "int"
|
|
// -group (-g): Distribution group name -- Default is "Private Release Script Group"
|
|
// -mandatory (-m): Should the release be mandatory ("true" or "false") -- Default is "false"
|
|
// -platform (-p): ios, android, or uwp -- Default is ios
|
|
|
|
// Read arguments
|
|
var environment = Environment.Prod;
|
|
if (Argument("Environment", "int") == "int")
|
|
{
|
|
environment = Environment.Int;
|
|
Token = EnvironmentVariable("APP_CENTER_INT_API_TOKEN");
|
|
BaseUrl = "https://api-gateway-core-integration.dev.avalanch.es";
|
|
}
|
|
var platformString = Argument<string>("Platform", "ios");
|
|
var platform = Platform.iOS;
|
|
if (platformString == "android")
|
|
{
|
|
platform = Platform.Android;
|
|
}
|
|
else if (platformString == "uwp")
|
|
{
|
|
platform = Platform.UWP;
|
|
}
|
|
|
|
CurrentApp = ( from app in Applications
|
|
where app.AppPlatform == platform &&
|
|
app.AppEnvironment == environment
|
|
select app
|
|
).Single();
|
|
|
|
DistributionGroup = Argument<string>("Group", DistributionGroup);
|
|
DistributionGroup = DistributionGroup.Replace('_', ' ');
|
|
IsMandatory = Argument<bool>("Mandatory", false);
|
|
});
|
|
|
|
// Distribution Tasks
|
|
|
|
Task("CreateIosArchive").IsDependentOn("IncreaseAppleVersion").Does(()=>
|
|
{
|
|
MSBuild(CurrentApp.ProjectPath, settings => settings.SetConfiguration("Release")
|
|
.WithTarget("Build")
|
|
.WithProperty("Platform", "iPhone")
|
|
.WithProperty("BuildIpa", "true")
|
|
.WithProperty("OutputPath", "bin/Release/")
|
|
.WithProperty("AllowUnsafeBlocks", "true"));
|
|
var projectLocation = CurrentApp.ProjectDirectory;
|
|
var ipaLocation = projectLocation +
|
|
"/bin/Release/" +
|
|
System.IO.Path.GetFileNameWithoutExtension(CurrentApp.ProjectPath) +
|
|
".ipa";
|
|
EnsureDirectoryExists(ArchiveDirectory);
|
|
if (System.IO.File.Exists(CurrentApp.AppPath))
|
|
{
|
|
System.IO.File.Delete(CurrentApp.AppPath);
|
|
}
|
|
CopyFile(ipaLocation, CurrentApp.AppPath);
|
|
});
|
|
|
|
Task("CreateAndroidArchive").IsDependentOn("IncreaseAndroidVersion").Does(()=>
|
|
{
|
|
BuildAndroidApk(CurrentApp.ProjectPath, true, "Release", c => c.Configuration = "Release");
|
|
var projectLocation = CurrentApp.ProjectDirectory;
|
|
var apks = GetFiles(projectLocation + "/bin/Release/*.apk");
|
|
var unsignedApk = "";
|
|
foreach (var path in apks)
|
|
{
|
|
if (!path.ToString().EndsWith("-Signed.apk"))
|
|
{
|
|
unsignedApk = path.ToString();
|
|
break;
|
|
}
|
|
}
|
|
EnsureDirectoryExists(ArchiveDirectory);
|
|
if (System.IO.File.Exists(CurrentApp.AppPath))
|
|
{
|
|
System.IO.File.Delete(CurrentApp.AppPath);
|
|
}
|
|
CopyFile(unsignedApk, CurrentApp.AppPath);
|
|
});
|
|
|
|
Task("IncreaseAppleVersion").Does(()=>
|
|
{
|
|
var infoPlistLocation = CurrentApp.ProjectDirectory + "/Info.plist";
|
|
var plist = File(infoPlistLocation);
|
|
var bundleVersionPattern = @"<key>CFBundleVersion<\/key>\s*<string>[^<]*<\/string>";
|
|
var match = FindRegexMatchInFile(File(infoPlistLocation), bundleVersionPattern, System.Text.RegularExpressions.RegexOptions.None);
|
|
var openTag = "<string>";
|
|
var closeTag = "</string>";
|
|
var currentVersion = match.Substring(match.IndexOf(openTag) + openTag.Length, match.IndexOf(closeTag) - match.IndexOf(openTag) - openTag.Length);
|
|
var newVersion = IncrementPatch(currentVersion);
|
|
var newBundleVersionString = "<key>CFBundleVersion</key>\n\t<string>" + newVersion + "</string>";
|
|
ReplaceRegexInFiles(infoPlistLocation, bundleVersionPattern, newBundleVersionString);
|
|
Information("iOS Version increased to " + newVersion);
|
|
});
|
|
|
|
Task("IncreaseAndroidVersion").Does(()=>
|
|
{
|
|
// Setup
|
|
var manifestLocation = CurrentApp.ProjectDirectory + "/Properties/AndroidManifest.xml";
|
|
var xmlNamespaces = new Dictionary<string, string> {{"android", "http://schemas.android.com/apk/res/android"}};
|
|
var peekSettings = new XmlPeekSettings();
|
|
peekSettings.Namespaces = xmlNamespaces;
|
|
var pokeSettings = new XmlPokeSettings();
|
|
pokeSettings.Namespaces = xmlNamespaces;
|
|
|
|
// Manifest version code
|
|
var versionCode = int.Parse(XmlPeek(manifestLocation, "manifest/@android:versionCode", peekSettings));
|
|
var newVersionCode = versionCode + 1;
|
|
XmlPoke(manifestLocation, "manifest/@android:versionCode", newVersionCode.ToString(), pokeSettings);
|
|
|
|
// Manifest version name
|
|
var versionName = XmlPeek(manifestLocation, "manifest/@android:versionName", peekSettings);
|
|
var suffix = "-SNAPSHOT";
|
|
if (versionName.Contains(suffix))
|
|
{
|
|
versionName = versionName.Substring(0, versionName.IndexOf(suffix));
|
|
}
|
|
var newVersionName = IncrementPatch(versionName);
|
|
XmlPoke(manifestLocation, "manifest/@android:versionName", newVersionName, pokeSettings);
|
|
Information("Android version name changed to " + newVersionName + ", version code increased to " + newVersionCode);
|
|
});
|
|
|
|
Task("ReleaseApplication")
|
|
.Does(()=>
|
|
{
|
|
if (CurrentApp.AppPlatform == Platform.iOS)
|
|
{
|
|
RunTarget("CreateIosArchive");
|
|
}
|
|
else if (CurrentApp.AppPlatform == Platform.Android)
|
|
{
|
|
RunTarget("CreateAndroidArchive");
|
|
}
|
|
else
|
|
{
|
|
Error("Cannot distribute for this platform.");
|
|
return;
|
|
}
|
|
|
|
// Start the upload.
|
|
Information("Initiating distribution process...");
|
|
var startUploadUrl = GetApiUrl(BaseUrl, CurrentApp.AppOwner, CurrentApp.AppId, "release_uploads");
|
|
var startUploadRequest = GetWebRequest(startUploadUrl, Token);
|
|
var startUploadResponse = GetResponseJson(startUploadRequest);
|
|
|
|
// Upload the file to the given endpoint. The label "ipa" is correct for all platforms.
|
|
var uploadUrl = startUploadResponse["upload_url"].ToString();
|
|
HttpUploadFile(uploadUrl, CurrentApp.AppPath, "ipa");
|
|
|
|
// Commit the upload
|
|
Information("Committing distribution...");
|
|
var uploadId = startUploadResponse["upload_id"].ToString();
|
|
var commitRequestUrl = startUploadUrl + "/" + uploadId;
|
|
var commitRequest = GetWebRequest(commitRequestUrl, Token, "PATCH");
|
|
AttachJsonPayload(commitRequest,
|
|
new JObject(
|
|
new JProperty("status", "committed")));
|
|
var commitResponse = GetResponseJson(commitRequest);
|
|
var releaseUrl = BaseUrl + "/" + commitResponse["release_url"].ToString();
|
|
|
|
// Release the upload
|
|
Information("Finalizing release...");
|
|
var releaseRequest = GetWebRequest(releaseUrl, Token, "PATCH");
|
|
var releaseNotes = "This release has been created by the script test-tools.cake.";
|
|
AttachJsonPayload(releaseRequest,
|
|
new JObject(
|
|
new JProperty("destination_name", DistributionGroup),
|
|
new JProperty("release_notes", releaseNotes),
|
|
new JProperty("mandatory", IsMandatory.ToString().ToLower())));
|
|
releaseRequest.GetResponse().Dispose();
|
|
var mandatorySuffix = IsMandatory ? " as a mandatory update" : "";
|
|
Information("Successfully released " + CurrentApp.AppOwner +
|
|
"/" + CurrentApp.AppId + " to group " +
|
|
DistributionGroup + mandatorySuffix + ".");
|
|
});
|
|
|
|
Task("BuildAppsInAppCenter").Does(() =>
|
|
{
|
|
CurrentApp = ( from app in Applications
|
|
where app.AppPlatform == Platform.iOS &&
|
|
app.AppEnvironment == Environment.Prod
|
|
select app
|
|
).Single();
|
|
BuildCurrentAppInAppCenter();
|
|
BuildCurrentAppInAppCenter();
|
|
CurrentApp = ( from app in Applications
|
|
where app.AppPlatform == Platform.Android &&
|
|
app.AppEnvironment == Environment.Prod
|
|
select app
|
|
).Single();
|
|
BuildCurrentAppInAppCenter();
|
|
BuildCurrentAppInAppCenter();
|
|
CurrentApp = ( from app in Applications
|
|
where app.AppPlatform == Platform.UWP &&
|
|
app.AppEnvironment == Environment.Prod
|
|
select app
|
|
).Single();
|
|
BuildCurrentAppInAppCenter();
|
|
});
|
|
|
|
void BuildCurrentAppInAppCenter()
|
|
{
|
|
Information("Triggering build in App Center... ");
|
|
var appCenterToken = Argument<string>("AppCenterToken");
|
|
var url = GetApiUrl(BaseUrl, CurrentApp.AppOwner, CurrentApp.AppId, "branches/master/builds");
|
|
var request = GetWebRequest(url, appCenterToken);
|
|
var responseJson = GetResponseJson(request);
|
|
Information("Successfully triggered build in App Center.");
|
|
}
|
|
|
|
// Helper methods
|
|
|
|
string GetApiUrl(string baseUrl, string appOwner, string appId, string apiName)
|
|
{
|
|
return string.Format("{0}/v0.1/apps/{1}/{2}/{3}", baseUrl, appOwner, appId, apiName);
|
|
}
|
|
|
|
JObject GetResponseJson(HttpWebRequest request)
|
|
{
|
|
using (var response = request.GetResponse())
|
|
using (var reader = new StreamReader(response.GetResponseStream()))
|
|
{
|
|
return JObject.Parse(reader.ReadToEnd());
|
|
}
|
|
}
|
|
|
|
HttpWebRequest GetWebRequest(string url, string token, string method = "POST")
|
|
{
|
|
Information(string.Format("About to call url '{0}'", url));
|
|
var request = (HttpWebRequest)WebRequest.Create(url);
|
|
request.Headers["X-API-Token"] = token;
|
|
request.ContentType = "application/json";
|
|
request.Accept = "application/json";
|
|
request.Method = method;
|
|
return request;
|
|
}
|
|
|
|
void AttachJsonPayload(HttpWebRequest request, JObject json)
|
|
{
|
|
using (var stream = request.GetRequestStream())
|
|
using (var sr = new StreamWriter(stream))
|
|
{
|
|
sr.Write(json.ToString());
|
|
}
|
|
}
|
|
|
|
string IncrementPatch(string semVer)
|
|
{
|
|
int patchIdx = 0;
|
|
for (int i = semVer.Length - 1; i >= 0; --i)
|
|
{
|
|
if (semVer[i] == '.')
|
|
{
|
|
patchIdx = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
var newPatch = Convert.ToInt32(semVer.Substring(patchIdx, semVer.Length - patchIdx)) + 1;
|
|
return semVer.Substring(0, patchIdx) + newPatch;
|
|
}
|
|
|
|
// Adapted from https://stackoverflow.com/questions/566462/upload-files-with-httpwebrequest-multipart-form-data/2996904#2996904
|
|
void HttpUploadFile(string url, string file, string paramName)
|
|
{
|
|
Information(string.Format("Uploading {0} to {1}", file, url));
|
|
var boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x");
|
|
byte[] boundaryBytes = System.Text.Encoding.ASCII.GetBytes("\r\n--" + boundary + "\r\n");
|
|
var request = (HttpWebRequest)WebRequest.Create(url);
|
|
request.ContentType = "multipart/form-data; boundary=" + boundary;
|
|
request.Method = "POST";
|
|
request.KeepAlive = true;
|
|
using (var requestStream = request.GetRequestStream())
|
|
{
|
|
requestStream.Write(boundaryBytes, 0, boundaryBytes.Length);
|
|
var headerTemplate = "Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"\r\n\r\n";
|
|
var header = string.Format(headerTemplate, paramName, file);
|
|
byte[] headerBytes = System.Text.Encoding.UTF8.GetBytes(header);
|
|
requestStream.Write(headerBytes, 0, headerBytes.Length);
|
|
using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read))
|
|
{
|
|
byte[] buffer = new byte[4096];
|
|
var bytesRead = 0;
|
|
while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0)
|
|
{
|
|
requestStream.Write(buffer, 0, bytesRead);
|
|
}
|
|
}
|
|
byte[] trailer = System.Text.Encoding.ASCII.GetBytes("\r\n--" + boundary + "--\r\n");
|
|
requestStream.Write(trailer, 0, trailer.Length);
|
|
}
|
|
request.GetResponse().Dispose();
|
|
Information("File uploaded.");
|
|
}
|
|
|
|
Task("Default").Does(()=>
|
|
{
|
|
Error("Please run a specific target.");
|
|
});
|
|
|
|
RunTarget(Target);
|