Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a Tool to connect to Google Play #5

Open
dansiegel opened this issue Jul 3, 2022 · 3 comments
Open

Add a Tool to connect to Google Play #5

dansiegel opened this issue Jul 3, 2022 · 3 comments
Labels
enhancement New feature or request

Comments

@dansiegel
Copy link
Member

Description

Add a tool to make it easy to connect to Google Play to deploy generated Android App Bundles.

@dansiegel dansiegel added the enhancement New feature or request label Jul 3, 2022
@bijington
Copy link

As discussed here is a basic implementation I have been using:

ReleaseMetadata.cs

public class ReleaseMetadata
{
    public string BundleFilePath { get; }

    public string PackageName { get; }

    public string ReleaseNotesFilePath { get; }

    public string SecretsFilePath { get; }

    public string TrackName { get; }

    public string Version { get; }

    public long VersionCode { get; }

    public ReleaseMetadata(
        string bundleFilePath,
        string packageName,
        string releaseNotesFilePath,
        string trackName,
        string version,
        long versionCode,
        string secretsFilePath)
    {
        this.BundleFilePath = bundleFilePath;
        this.PackageName = packageName;
        this.ReleaseNotesFilePath = releaseNotesFilePath;
        this.TrackName = trackName;
        this.Version = version;
        this.VersionCode = versionCode;
        this.SecretsFilePath = secretsFilePath;
    }
}

Upload method

private static async Task Run(ReleaseMetadata releaseMetadata)
{
    var credential = GoogleCredential
        .FromFile(releaseMetadata.SecretsFilePath)
        .CreateScoped(AndroidPublisherService.Scope.Androidpublisher);

    var service = new AndroidPublisherService(new BaseClientService.Initializer
    {
        ApplicationName = "App Uploader",
        HttpClientInitializer = credential
    });

    var appEdit = new AppEdit();

    // Request an insert.
    await Console.Out.WriteLineAsync("Creating Edits.Insert");
    var insertRequest =
        await service.Edits.Insert(appEdit, releaseMetadata.PackageName).ExecuteAsync();

    var editId = insertRequest.Id;

    // Upload the bundle
    await using var fileStream = new FileStream(releaseMetadata.BundleFilePath, FileMode.Open, FileAccess.Read);

    await Console.Out.WriteLineAsync("Uploading bundle file");
    var uploadResult = await service.Edits.Bundles.Upload(releaseMetadata.PackageName, editId, fileStream, "").UploadAsync();

    if (uploadResult.Status != UploadStatus.Completed)
    {
        await Console.Error.WriteLineAsync($"ERROR: Upload status '{uploadResult.Status}' with exception '{uploadResult.Exception}'");
        return;
    }

    // Set up the next release.
    await Console.Out.WriteLineAsync("Creating new Track release");
    var currentTrack = await service.Edits.Tracks.Get(releaseMetadata.PackageName, editId, releaseMetadata.TrackName).ExecuteAsync();

    var versionCodes = new List<long?> { releaseMetadata.VersionCode };

    await using var releaseNotesStream = new FileStream(releaseMetadata.ReleaseNotesFilePath, FileMode.Open, FileAccess.Read);
    using var streamReader = new StreamReader(releaseNotesStream);

    var releaseNotes = await streamReader.ReadToEndAsync();

    if (releaseNotes.Length > 500)
    {
        await Console.Out.WriteLineAsync("WARNING: truncated the release notes");
        // Yuck!!
        releaseNotes = releaseNotes.Substring(0, 500);
    }

    // Currently relies on having manually released initially
    var newReleaseTrack = new TrackRelease
    {
        CountryTargeting = currentTrack.Releases.First().CountryTargeting,
        Name = $"{releaseMetadata.VersionCode}({releaseMetadata.Version})",
        VersionCodes = versionCodes,
        ReleaseNotes = new List<LocalizedText> { new LocalizedText { Language = "en-GB", Text = releaseNotes } },
        Status = "completed"
    };

    currentTrack.Releases.Clear();
    currentTrack.Releases.Add(newReleaseTrack);

    await Console.Out.WriteLineAsync($"Updating track '{releaseMetadata.TrackName}'");
    await service.Edits.Tracks.Update(currentTrack, releaseMetadata.PackageName, editId, releaseMetadata.TrackName).ExecuteAsync();

    // Job done
    await Console.Out.WriteLineAsync("Committing release");
    await service.Edits.Commit(releaseMetadata.PackageName, editId).ExecuteAsync();
    }
}

Usage

private async static Task Main(string[] args)
{
    var assembly = Assembly.GetExecutingAssembly();
    var fvi = FileVersionInfo.GetVersionInfo(assembly.Location);

    Console.Error.WriteLine($"Running Google.Play.Uploader {fvi.ProductVersion}");

    // TODO: Better argument parsing
    var bundleFilePath = args[0];
    var packageName = args[1];
    var trackName = args[2];
    var releaseNotesFilePath = args[3]; 
    var version = args[4];
    var versionCode = long.Parse(args[5]);
    var secretsFilePath = args[6];

    Console.Error.WriteLine($"Uploading Android bundle: '{bundleFilePath}' for package name: '{packageName}' to Google Play");

    var releaseMetadata = new ReleaseMetadata(
        bundleFilePath,
        packageName,
        releaseNotesFilePath,
        trackName,
        version,
        versionCode,
        secretsFilePath);

    try
    {
        await Run(releaseMetadata);
    }
    catch (Exception ex)
    {
        Console.Error.WriteLine("ERROR: " + ex.Message);

        // Make sure TeamCity observes the failure.
        Environment.Exit(-1);
    }
}

This doesn't take care of handling localised release notes.

@bijington
Copy link

I'm also very happy to add the implementation but thought I'd show something

@dansiegel
Copy link
Member Author

Yeah looks like a good start. If localization (see what I did there) is a big deal someone can PR it... but should be easy enough to adapt something like:

public interface IHazGooglePlayDeployment : IHazArtifacts
{
    [Parameter("Defines the Google Play Track to deploy to")]
    GooglePlayTrack GooglePlayTrack => TryGetValue(() => GooglePlayTrack) ?? GooglePlayTrack.Internal;

    [Parameter("Secrets file to connect to Google Play"), Secret]
    string GooglePlaySecrets => TryGetValue(() => GooglePlaySecrets);

    Target GooglePlayDeploy => _ => _
        .Requires(() => GooglePlaySecrets)
        .Executes(async () =>
        {
            var aab = ArtifactsDirectory.GlobFiles("**/*-Signed.aab").FirstOrDefault();
            aab.NotNull().FileExists();
            // Pull the aab apart for the AndroidManifest and read the Manifest for the Version/Version Code

            // Do release
        });
}

public class GooglePlayTrack : Enumeration
{
    public static GooglePlayTrack Internal = new() { Value = nameof(Internal) };
    public static GooglePlayTrack Alpha = new() { Value = nameof(Alpha) };
    public static GooglePlayTrack Beta = new() { Value = nameof(Beta) };
    public static GooglePlayTrack Production = new() { Value = nameof(Production) };
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants