Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NuGet.Config
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<clear />
<add key="dotnet7" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet7/nuget/v3/index.json" />
<add key="dotnet-public" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json" />
<add key="dotnet-libraries" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries/nuget/v3/index.json" />
<add key="myget-legacy" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/myget-legacy/nuget/v3/index.json" />
</packageSources>
<packageRestore>
Expand Down
180 changes: 180 additions & 0 deletions src/cijobs/CIClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

namespace ManagedCodeGen
{
// Wrap CI httpClient with focused APIs for product, job, and build.
// This logic is seperate from listing/copying and just extracts data.
internal sealed class CIClient
{
private HttpClient _client;

public CIClient(string server)
{
_client = new HttpClient();
_client.BaseAddress = new Uri(server);
_client.DefaultRequestHeaders.Accept.Clear();
_client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
_client.Timeout = Timeout.InfiniteTimeSpan;
}

public async Task<bool> DownloadProduct(string messageString, string outputPath, string contentPath)
{
Console.WriteLine("Downloading: {0}", messageString);

HttpResponseMessage response = await _client.GetAsync(messageString);

bool downloaded = false;

if (response.IsSuccessStatusCode)
{
var zipPath = Path.Combine(outputPath, Path.GetFileName(contentPath));
using (var outputStream = System.IO.File.Create(zipPath))
{
Stream inputStream = await response.Content.ReadAsStreamAsync();
inputStream.CopyTo(outputStream);
}
downloaded = true;
}
else
{
Console.Error.WriteLine("Zip not found!");
}

return downloaded;
}

public async Task<IEnumerable<Job>> GetProductJobs(string productName, string branchName)
{
string productString = $"job/{productName}/job/{branchName}/api/json?&tree=jobs[name,url]";

try
{
using HttpResponseMessage response = await _client.GetAsync(productString);

if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync();
var productJobs = JsonSerializer.Deserialize<ProductJobs>(json);
return productJobs.jobs;
}
}
catch (Exception ex)
{
Console.Error.WriteLine("Error enumerating jobs: {0} {1}", ex.Message, ex.InnerException.Message);
}

return Enumerable.Empty<Job>();
}

public async Task<IEnumerable<Build>> GetJobBuilds(string productName, string branchName,
string jobName, bool lastSuccessfulBuild, int number, string commit)
{
var jobString
= String.Format(@"job/{0}/job/{1}/job/{2}", productName, branchName, jobName);
var messageString
= String.Format("{0}/api/json?&tree=builds[number,url],lastSuccessfulBuild[number,url]",
jobString);
HttpResponseMessage response = await _client.GetAsync(messageString);

if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync();
var jobBuilds = JsonSerializer.Deserialize<JobBuilds>(json);

if (lastSuccessfulBuild)
{
var lastSuccessfulNumber = jobBuilds.lastSuccessfulBuild.number;
jobBuilds.lastSuccessfulBuild.info = await GetJobBuildInfo(productName, branchName, jobName, lastSuccessfulNumber);
return Enumerable.Repeat(jobBuilds.lastSuccessfulBuild, 1);
}
else if (number != 0)
{
var builds = jobBuilds.builds;

var count = builds.Count();
for (int i = 0; i < count; i++)
{
var build = builds[i];
if (build.number == number)
{
build.info = await GetJobBuildInfo(productName, branchName, jobName, build.number);
return Enumerable.Repeat(build, 1);
}
}
return Enumerable.Empty<Build>();
}
else if (commit != null)
{
var builds = jobBuilds.builds;

var count = builds.Count();
for (int i = 0; i < count; i++)
{
var build = builds[i];
build.info = await GetJobBuildInfo(productName, branchName, jobName, build.number);
var actions = build.info.actions.Where(x => x.lastBuiltRevision.SHA1 != null);
foreach (var action in actions)
{
if (action.lastBuiltRevision.SHA1.Equals(commit, StringComparison.OrdinalIgnoreCase))
{
return Enumerable.Repeat(build, 1);
}
}
}
return Enumerable.Empty<Build>();
}
else
{
var builds = jobBuilds.builds;

var count = builds.Count();
for (int i = 0; i < count; i++)
{
var build = builds[i];
// fill in build info
build.info = await GetJobBuildInfo(productName, branchName, jobName, build.number);
builds[i] = build;
}

return jobBuilds.builds;
}
}
else
{
return Enumerable.Empty<Build>();
}
}

public async Task<BuildInfo> GetJobBuildInfo(string repoName, string branchName, string jobName, int number)
{
string buildString = String.Format("job/{0}/job/{1}/job/{2}/{3}",
repoName, branchName, jobName, number);
string buildMessage = String.Format("{0}/{1}", buildString,
"api/json?&tree=actions[lastBuiltRevision[SHA1]],artifacts[fileName,relativePath],result");
HttpResponseMessage response = await _client.GetAsync(buildMessage);

if (response.IsSuccessStatusCode)
{
var buildInfoJson = await response.Content.ReadAsStringAsync();
var info = JsonSerializer.Deserialize<BuildInfo>(buildInfoJson);
return info;
}
else
{
return null;
}
}
}
}
188 changes: 188 additions & 0 deletions src/cijobs/CIJobsRootCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Threading.Tasks;

namespace ManagedCodeGen
{
internal sealed class CIJobsRootCommand : RootCommand
{
public Option<string> Server { get; } =
new(new[] { "--server", "-s" }, "Url of the server. Defaults to http://ci.dot.net/");
public Option<string> JobName { get; } =
new(new[] { "--job", "-j" }, "Name of the job.");
public Option<string> BranchName { get; } =
new(new[] { "--branch", "-b" }, _ => "master", true, "Name of the branch.");
public Option<string> RepoName { get; } =
new(new[] { "--repo", "-r" }, _ => "dotnet_coreclr", true, "Name of the repo (e.g. dotnet_corefx or dotnet_coreclr).");
public Option<string> MatchPattern { get; } =
new(new[] { "--match", "-m" }, "Regex pattern used to select jobs output.");
public Option<int> JobNumber { get; } =
new(new[] { "--number", "-n" }, "Job number.");
public Option<bool> ShowLastSuccessful { get; } =
new(new[] { "--last-successful", "-l", }, "Show last successful build.");
public Option<string> Commit { get; } =
new(new[] { "--commit", "-c", }, "List build at this commit.");
public Option<bool> ShowArtifacts { get; } =
new(new[] { "--artifacts", "-a" }, "Show job artifacts on server.");
public Option<string> OutputPath { get; } =
new(new[] { "--output", "-o" }, "The path where output will be placed.");
public Option<string> OutputRoot { get; } =
new("--output-root", "The root directory where output will be placed. A subdirectory named by job and build number will be created within this to store the output.");
public Option<bool> Unzip { get; } =
new(new[] { "--unzip", "-u" }, "Unzip copied artifacts");
public Option<string> ContentPath { get; } =
new(new[] { "--ContentPath", "-p" }, "Relative product zip path. Default is artifact/bin/Product/*zip*/Product.zip");

public ParseResult Result;

public CIJobsRootCommand(string[] args) : base("Continuous integration build jobs tool")
{
List<string> errors = new();

Command listCommand = new("list", "List jobs on dotnet-ci.cloudapp.net for the repo.")
{
Server,
JobName,
BranchName,
RepoName,
MatchPattern,
JobNumber,
ShowLastSuccessful,
Commit,
ShowArtifacts
};

listCommand.SetHandler(context => TryExecuteWithContextAsync(context, "list", result =>
{
int jobNumber = result.GetValueForOption(JobNumber);
bool showLastSuccessful = result.GetValueForOption(ShowLastSuccessful);
string commit = result.GetValueForOption(Commit);

if (result.FindResultFor(JobNumber) == null)
{
if (jobNumber != 0)
{
errors.Add("Must select --job <name> to specify --number <num>.");
}

if (showLastSuccessful)
{
errors.Add("Must select --job <name> to specify --last_successful.");
}

if (commit != null)
{
errors.Add("Must select --job <name> to specify --commit <commit>.");
}

if (result.GetValueForOption(ShowArtifacts))
{
errors.Add("Must select --job <name> to specify --artifacts.");
}
}
else
{
if (Convert.ToInt32(jobNumber != 0) + Convert.ToInt32(showLastSuccessful) + Convert.ToInt32(commit != null) > 1)
{
errors.Add("Must have at most one of --number <num>, --last_successful, and --commit <commit> for list.");
}

if (!string.IsNullOrEmpty(result.GetValueForOption(MatchPattern)))
{
errors.Add("Match pattern not valid with --job");
}
}
}));

AddCommand(listCommand);

Command copyCommand = new("copy", @"Copies job artifacts from dotnet-ci.cloudapp.net. This
command copies a zip of artifacts from a repo (defaulted to
dotnet_coreclr). The default location of the zips is the
Product sub-directory, though that can be changed using the
ContentPath(p) parameter")
{
Server,
JobName,
BranchName,
RepoName,
JobNumber,
ShowLastSuccessful,
Commit,
ShowArtifacts,
OutputPath,
OutputRoot,
Unzip,
ContentPath
};

copyCommand.SetHandler(context => TryExecuteWithContextAsync(context, "copy", result =>
{
if (result.GetValueForOption(JobName) == null)
{
errors.Add("Must have --job <name> for copy.");
}

int jobNumber = result.GetValueForOption(JobNumber);
bool shwoLastSuccessful = result.GetValueForOption(ShowLastSuccessful);
string commit = result.GetValueForOption(Commit);
if (jobNumber == 0 && !shwoLastSuccessful && commit == null)
{
errors.Add("Must have --number <num>, --last_successful, or --commit <commit> for copy.");
}

if (Convert.ToInt32(jobNumber != 0) + Convert.ToInt32(shwoLastSuccessful) + Convert.ToInt32(commit != null) > 1)
{
errors.Add("Must have only one of --number <num>, --last_successful, and --commit <commit> for copy.");
}

string outputPath = result.GetValueForOption(OutputPath);
string outputRoot = result.GetValueForOption(OutputRoot);
if (outputPath == null && outputRoot == null)
{
errors.Add("Must specify either --output <path> or --output_root <path> for copy.");
}

if (outputPath != null && outputRoot != null)
{
errors.Add("Must specify only one of --output <path> or --output_root <path>.");
}
}));

AddCommand(copyCommand);

async Task TryExecuteWithContextAsync(InvocationContext context, string name, Action<ParseResult> validate)
{
Result = context.ParseResult;
try
{
validate(Result);
if (errors.Count > 0)
{
throw new Exception(string.Join(Environment.NewLine, errors));
}

context.ExitCode = await new Program(this).RunAsync(name);
}
catch (Exception e)
{
Console.ResetColor();
Console.ForegroundColor = ConsoleColor.Red;

Console.Error.WriteLine("Error: " + e.Message);
Console.Error.WriteLine(e.ToString());

Console.ResetColor();

context.ExitCode = 1;
}
}
}
}
}
Loading