Add discord rich embeds for deployment messages

This commit is contained in:
Jordan Brown
2020-05-24 22:44:24 -04:00
parent ed4b485cc2
commit 6473cec81b
9 changed files with 507 additions and 246 deletions
@@ -620,13 +620,61 @@ namespace Tgstation.Server.Host.Components.Chat
}
/// <inheritdoc />
public Task SendUpdateMessage(string message, CancellationToken cancellationToken)
public async Task<Func<string, string, Task>> SendDeploymentMessage(
Models.RevisionInformation revisionInformation,
Version byondVersion,
DateTimeOffset? estimatedCompletionTime,
string gitHubOwner,
string gitHubRepo,
bool localCommitPushed,
CancellationToken cancellationToken)
{
List<ulong> wdChannels;
message = String.Format(CultureInfo.InvariantCulture, "DM: {0}", message);
lock (mappedChannels) // so it doesn't change while we're using it
wdChannels = mappedChannels.Where(x => x.Value.IsUpdatesChannel).Select(x => x.Key).ToList();
return SendMessage(message, wdChannels, cancellationToken);
logger.LogTrace("Sending deployment message for RevisionInformation: {0}", revisionInformation.Id);
var callbacks = new List<Func<string, string, Task>>();
await Task.WhenAll(
wdChannels.Select(
async x =>
{
ChannelMapping channelMapping;
lock (mappedChannels)
if (!mappedChannels.TryGetValue(x, out channelMapping))
return;
IProvider provider;
lock (providers)
if (!providers.TryGetValue(channelMapping.ProviderId, out provider))
return;
try
{
var callback = await provider.SendUpdateMessage(
revisionInformation,
byondVersion,
estimatedCompletionTime,
gitHubOwner,
gitHubRepo,
channelMapping.ProviderChannelId,
localCommitPushed,
cancellationToken)
.ConfigureAwait(false);
callbacks.Add(callback);
}
catch (Exception ex)
{
logger.LogWarning(
"Error sending deploy message to provider {0}! Exception: {1}",
channelMapping.ProviderId,
ex);
}
}))
.ConfigureAwait(false);
return (errorMessage, dreamMakerOutput) => Task.WhenAll(callbacks.Select(x => x(errorMessage, dreamMakerOutput)));
}
/// <inheritdoc />
@@ -62,12 +62,24 @@ namespace Tgstation.Server.Host.Components.Chat
Task SendWatchdogMessage(string message, bool adminOnly, CancellationToken cancellationToken);
/// <summary>
/// Send a chat <paramref name="message"/> to configured update channels
/// Send the message for a deployment to configured deployment channels.
/// </summary>
/// <param name="message">The message being sent</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task SendUpdateMessage(string message, CancellationToken cancellationToken);
/// <param name="revisionInformation">The <see cref="RevisionInformation"/> of the deployment.</param>
/// <param name="byondVersion">The BYOND <see cref="Version"/> of the deployment.</param>
/// <param name="estimatedCompletionTime">The optional <see cref="DateTimeOffset"/> the deployment is expected to be completed at.</param>
/// <param name="gitHubOwner">The repository GitHub owner, if any.</param>
/// <param name="gitHubRepo">The repository GitHub name, if any.</param>
/// <param name="localCommitPushed"><see langword="true"/> if the local deployment commit was pushed to the remote repository.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="Func{T1, T2, TResult}"/> to call to update the message at the deployment's conclusion. Parameters: Error message if any, DreamMaker output if any.</returns>
Task<Func<string, string, Task>> SendDeploymentMessage(
Models.RevisionInformation revisionInformation,
Version byondVersion,
DateTimeOffset? estimatedCompletionTime,
string gitHubOwner,
string gitHubRepo,
bool localCommitPushed,
CancellationToken cancellationToken);
/// <summary>
/// Start tracking <see cref="Commands.CustomCommand"/>s and <see cref="ChannelRepresentation"/>s.
@@ -6,6 +6,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.Components.Chat.Providers
{
@@ -28,6 +30,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
}
/// <summary>
/// The <see cref="IAssemblyInformationProvider"/> for the <see cref="DiscordProvider"/>.
/// </summary>
readonly IAssemblyInformationProvider assemblyInformationProvider;
/// <summary>
/// The <see cref="DiscordSocketClient"/> for the <see cref="DiscordProvider"/>
/// </summary>
@@ -53,12 +60,18 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <summary>
/// Construct a <see cref="DiscordProvider"/>
/// </summary>
/// <param name="assemblyInformationProvider">The value of <see cref="assemblyInformationProvider"/>.</param>
/// <param name="logger">The value of <see cref="Logger"/></param>
/// <param name="botToken">The value of <see cref="botToken"/></param>
/// <param name="reconnectInterval">The initial reconnect interval in minutes.</param>
public DiscordProvider(ILogger<DiscordProvider> logger, string botToken, uint reconnectInterval)
public DiscordProvider(
IAssemblyInformationProvider assemblyInformationProvider,
ILogger<DiscordProvider> logger,
string botToken,
uint reconnectInterval)
: base(logger, reconnectInterval)
{
this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
this.botToken = botToken ?? throw new ArgumentNullException(nameof(botToken));
client = new DiscordSocketClient();
client.MessageReceived += Client_MessageReceived;
@@ -260,5 +273,138 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
Logger.LogWarning("Error sending discord message: {0}", e);
}
}
/// <inheritdoc />
public override async Task<Func<string, string, Task>> SendUpdateMessage(
RevisionInformation revisionInformation,
Version byondVersion,
DateTimeOffset? estimatedCompletionTime,
string gitHubOwner,
string gitHubRepo,
ulong channelId,
bool localCommitPushed,
CancellationToken cancellationToken)
{
bool gitHub = gitHubOwner != null && gitHubRepo != null;
localCommitPushed |= revisionInformation.CommitSha == revisionInformation.OriginCommitSha;
var fields = new List<EmbedFieldBuilder>
{
new EmbedFieldBuilder
{
Name = "BYOND Version",
Value = $"{byondVersion.Major}.{byondVersion.Minor}",
IsInline = true
},
new EmbedFieldBuilder
{
Name = "Local Commit",
Value = localCommitPushed && gitHub
? $"[{revisionInformation.CommitSha.Substring(0, 7)}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionInformation.CommitSha})"
: revisionInformation.CommitSha.Substring(0, 7),
IsInline = true
},
new EmbedFieldBuilder
{
Name = "Branch Commit",
Value = gitHub
? $"[{revisionInformation.OriginCommitSha.Substring(0, 7)}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionInformation.OriginCommitSha})"
: revisionInformation.OriginCommitSha.Substring(0, 7),
IsInline = true
}
};
fields.AddRange((revisionInformation.ActiveTestMerges ?? Enumerable.Empty<RevInfoTestMerge>())
.Select(x => x.TestMerge)
.Select(x => new EmbedFieldBuilder
{
Name = $"#{x.Number}",
Value = $"[{x.TitleAtMerge}]({x.Url}) by _[@{x.Author}](https://github.com/{x.Author})_{Environment.NewLine}Commit: [{x.PullRequestRevision.Substring(0, 7)}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{x.PullRequestRevision}){(String.IsNullOrWhiteSpace(x.Comment) ? String.Empty : $"{Environment.NewLine}_**{x.Comment}**_")}"
}));
var builder = new EmbedBuilder
{
Author = new EmbedAuthorBuilder
{
Name = assemblyInformationProvider.VersionPrefix,
Url = "https://github.com/tgstation/tgstation-server",
IconUrl = "https://avatars0.githubusercontent.com/u/1363778?s=280&v=4"
},
Color = Color.Gold,
Description = "TGS has begun deploying active repository code to production.",
Fields = fields,
Title = "Code Deployment",
Footer = new EmbedFooterBuilder
{
Text = "In progress... ETA"
},
Timestamp = estimatedCompletionTime
};
Logger.LogTrace("Attempting to post deploy embed to channel {0}...", channelId);
if (!(client.GetChannel(channelId) is IMessageChannel channel))
{
Logger.LogTrace("Channel ID {0} does not exist or is not an IMessageChannel!", channelId);
return (errorMessage, dreamMakerOutput) => Task.CompletedTask;
}
var message = await channel.SendMessageAsync(
String.Empty,
false,
builder.Build(),
new RequestOptions
{
CancelToken = cancellationToken
})
.ConfigureAwait(false);
return async (errorMessage, dreamMakerOutput) =>
{
builder.Footer.Text = errorMessage == null ? "Succeeded" : "Failed";
builder.Color = errorMessage == null ? Color.Green : Color.Red;
builder.Timestamp = DateTimeOffset.Now;
builder.Description = errorMessage == null
? "The deployment completed successfully and will be available at the next server reboot."
: "The deployment failed.";
if (dreamMakerOutput != null)
builder.AddField(new EmbedFieldBuilder
{
Name = "DreamMaker Output",
Value = $"```{Environment.NewLine}{dreamMakerOutput}{Environment.NewLine}```"
});
if (errorMessage != null)
builder.AddField(new EmbedFieldBuilder
{
Name = "Error Message",
Value = errorMessage
});
try
{
await message.ModifyAsync(
props => props.Embed = builder.Build())
.ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogWarning("Updating deploy embed {0} failed, attempting new post! Exception: {1}", message.Id, ex);
try
{
await channel.SendMessageAsync(
String.Empty,
false,
builder.Build())
.ConfigureAwait(false);
}
catch (Exception ex2)
{
Logger.LogWarning("Posting completion deploy embed failed! Exception: {0}", ex2);
}
}
};
}
}
}
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Components.Chat.Providers
{
@@ -65,5 +66,27 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <param name="reconnectInterval">The reconnection interval in minutes.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task SetReconnectInterval(uint reconnectInterval);
/// <summary>
/// Send the message for a deployment.
/// </summary>
/// <param name="revisionInformation">The <see cref="RevisionInformation"/> of the deployment.</param>
/// <param name="byondVersion">The BYOND <see cref="Version"/> of the deployment.</param>
/// <param name="estimatedCompletionTime">The optional <see cref="DateTimeOffset"/> the deployment is expected to be completed at.</param>
/// <param name="gitHubOwner">The repository GitHub owner, if any.</param>
/// <param name="gitHubRepo">The repository GitHub name, if any.</param>
/// <param name="channelId">The <see cref="ChannelRepresentation.RealId"/> to send to</param>
/// <param name="localCommitPushed"><see langword="true"/> if the local deployment commit was pushed to the remote repository.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in a <see cref="Func{T1, T2, TResult}"/> to call to update the message at the deployment's conclusion. Parameters: Error message if any, DreamMaker output if any.</returns>
Task<Func<string, string, Task>> SendUpdateMessage(
RevisionInformation revisionInformation,
Version byondVersion,
DateTimeOffset? estimatedCompletionTime,
string gitHubOwner,
string gitHubRepo,
ulong channelId,
bool localCommitPushed,
CancellationToken cancellationToken);
}
}
@@ -467,5 +467,55 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
Logger.LogWarning("Unable to send to channel: {0}", e);
}
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
/// <inheritdoc />
public override async Task<Func<string, string, Task>> SendUpdateMessage(
Models.RevisionInformation revisionInformation,
Version byondVersion,
DateTimeOffset? estimatedCompletionTime,
string gitHubOwner,
string gitHubRepo,
ulong channelId,
bool localCommitPushed,
CancellationToken cancellationToken)
{
var commitInsert = revisionInformation.CommitSha.Substring(0, 7);
string remoteCommitInsert;
if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha)
{
commitInsert = String.Format(CultureInfo.InvariantCulture, localCommitPushed ? "^{0}" : "{0}", commitInsert);
remoteCommitInsert = String.Empty;
}
else
remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha.Substring(0, 7));
var testmergeInsert = (revisionInformation.ActiveTestMerges?.Count ?? 0) == 0 ? String.Empty : String.Format(CultureInfo.InvariantCulture, " (Test Merges: {0})",
String.Join(", ", revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x =>
{
var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.PullRequestRevision.Substring(0, 7));
if (x.Comment != null)
result += String.Format(CultureInfo.InvariantCulture, " ({0})", x.Comment);
return result;
})));
await SendMessage(
channelId,
String.Format(
CultureInfo.InvariantCulture,
"DM: Deploying revision: {0}{1}{2} BYOND Version: {3}{4}",
commitInsert,
testmergeInsert,
remoteCommitInsert,
byondVersion,
estimatedCompletionTime.HasValue
? $" ETA: {estimatedCompletionTime - DateTimeOffset.Now}"
: String.Empty),
cancellationToken).ConfigureAwait(false);
return (errorMessage, dreamMakerOutput) => SendMessage(
channelId,
$"DM: Deployment {(errorMessage == null ? "complete" : "failed")}!",
cancellationToken);
}
}
}
@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Components.Chat.Providers
{
@@ -180,5 +181,16 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// <inheritdoc />
public abstract Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken);
/// <inheritdoc />
public abstract Task<Func<string, string, Task>> SendUpdateMessage(
RevisionInformation revisionInformation,
Version byondVersion,
DateTimeOffset? estimatedCompletionTime,
string gitHubOwner,
string gitHubRepo,
ulong channelId,
bool localCommitPushed,
CancellationToken cancellationToken);
}
}
@@ -56,7 +56,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
return new IrcProvider(assemblyInformationProvider, asyncDelayer, loggerFactory.CreateLogger<IrcProvider>(), ircBuilder.Address, ircBuilder.Port.Value, ircBuilder.Nickname, ircBuilder.Password, ircBuilder.PasswordType, settings.ReconnectionInterval.Value, ircBuilder.UseSsl.Value);
case ChatProvider.Discord:
var discordBuilder = (DiscordConnectionStringBuilder)builder;
return new DiscordProvider(loggerFactory.CreateLogger<DiscordProvider>(), discordBuilder.BotToken, settings.ReconnectionInterval.Value);
return new DiscordProvider(
assemblyInformationProvider,
loggerFactory.CreateLogger<DiscordProvider>(),
discordBuilder.BotToken,
settings.ReconnectionInterval.Value);
default:
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid ChatProvider: {0}", settings.Provider));
}
@@ -14,7 +14,6 @@ using Tgstation.Server.Host.Components.Byond;
using Tgstation.Server.Host.Components.Chat;
using Tgstation.Server.Host.Components.Repository;
using Tgstation.Server.Host.Components.Session;
using Tgstation.Server.Host.Components.Watchdog;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.IO;
@@ -82,11 +81,6 @@ namespace Tgstation.Server.Host.Components.Deployment
/// </summary>
readonly IProcessExecutor processExecutor;
/// <summary>
/// The <see cref="IWatchdog"/> for <see cref="DreamMaker"/>
/// </summary>
readonly IWatchdog watchdog;
/// <summary>
/// The <see cref="IRepositoryManager"/> for <see cref="DreamMaker"/>.
/// </summary>
@@ -113,14 +107,18 @@ namespace Tgstation.Server.Host.Components.Deployment
readonly Api.Models.Instance metadata;
/// <summary>
/// <see langword="lock"/> <see cref="object"/> for <see cref="compiling"/>.
/// <see langword="lock"/> <see cref="object"/> for <see cref="deploying"/>.
/// </summary>
readonly object compilingLock;
readonly object deploymentLock;
Func<string, string, Task> currentChatCallback;
string currentDreamMakerOutput;
/// <summary>
/// If a compile job is running
/// </summary>
bool compiling;
bool deploying;
/// <summary>
/// Construct <see cref="DreamMaker"/>
@@ -132,7 +130,6 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <param name="eventConsumer">The value of <see cref="eventConsumer"/></param>
/// <param name="chatManager">The value of <see cref="chatManager"/></param>
/// <param name="processExecutor">The value of <see cref="processExecutor"/></param>
/// <param name="watchdog">The value of <see cref="watchdog"/></param>
/// <param name="gitHubClientFactory">The value of <see cref="gitHubClientFactory"/>.</param>
/// <param name="compileJobConsumer">The value of <see cref="compileJobConsumer"/>.</param>
/// <param name="repositoryManager">The value of <see cref="repositoryManager"/>.</param>
@@ -146,7 +143,6 @@ namespace Tgstation.Server.Host.Components.Deployment
IEventConsumer eventConsumer,
IChatManager chatManager,
IProcessExecutor processExecutor,
IWatchdog watchdog,
IGitHubClientFactory gitHubClientFactory,
ICompileJobSink compileJobConsumer,
IRepositoryManager repositoryManager,
@@ -160,14 +156,13 @@ namespace Tgstation.Server.Host.Components.Deployment
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
this.chatManager = chatManager ?? throw new ArgumentNullException(nameof(chatManager));
this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
this.watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
this.compileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer));
this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
compilingLock = new object();
deploymentLock = new object();
}
/// <summary>
@@ -289,7 +284,7 @@ namespace Tgstation.Server.Host.Components.Deployment
cancellationToken.ThrowIfCancellationRequested();
logger.LogDebug("DreamMaker exit code: {0}", exitCode);
job.Output = dm.GetCombinedOutput();
currentDreamMakerOutput = job.Output = dm.GetCombinedOutput();
logger.LogDebug("DreamMaker output: {0}{1}", Environment.NewLine, job.Output);
return exitCode;
}
@@ -352,13 +347,10 @@ namespace Tgstation.Server.Host.Components.Deployment
/// Cleans up a failed compile <paramref name="job"/>
/// </summary>
/// <param name="job">The running <see cref="CompileJob"/></param>
/// <param name="cancelled">If the <paramref name="job"/> was cancelled</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
async Task CleanupFailedCompile(Models.CompileJob job, bool cancelled, CancellationToken cancellationToken)
async Task CleanupFailedCompile(Models.CompileJob job)
{
logger.LogTrace("Cleaning compile directory...");
var chatTask = chatManager.SendUpdateMessage(cancelled ? "Deploy cancelled!" : "Deploy failed!", cancellationToken);
var jobPath = job.DirectoryName.ToString();
try
{
@@ -368,66 +360,6 @@ namespace Tgstation.Server.Host.Components.Deployment
{
logger.LogWarning("Error cleaning up compile directory {0}! Exception: {1}", ioManager.ResolvePath(jobPath), e);
}
await chatTask.ConfigureAwait(false);
}
/// <summary>
/// Send a message to <see cref="chatManager"/> about a deployment
/// </summary>
/// <param name="revisionInformation">The <see cref="RevisionInformation"/> for the deployment</param>
/// <param name="byondLock">The <see cref="IByondExecutableLock"/> for the deployment</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
async Task SendDeploymentMessage(Models.RevisionInformation revisionInformation, IByondExecutableLock byondLock, CancellationToken cancellationToken)
{
var commitInsert = revisionInformation.CommitSha.Substring(0, 7);
string remoteCommitInsert;
if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha)
{
commitInsert = String.Format(CultureInfo.InvariantCulture, "^{0}", commitInsert);
remoteCommitInsert = String.Empty;
}
else
remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha.Substring(0, 7));
var testmergeInsert = (revisionInformation.ActiveTestMerges?.Count ?? 0) == 0
? String.Empty
: String.Format(
CultureInfo.InvariantCulture,
"{0}Test Merges:{1}",
Environment.NewLine,
String.Join(
Environment.NewLine,
revisionInformation
.ActiveTestMerges
.Select(x => x.TestMerge)
.Select(x =>
{
var result = String.Format(
CultureInfo.InvariantCulture,
"- #{0} at {1}",
x.Number,
x.PullRequestRevision.Substring(0, 7));
if (x.Comment != null)
result += $": {x.Comment}";
return result;
})));
await chatManager.SendUpdateMessage(
String.Format(
CultureInfo.InvariantCulture,
"*Deployment Triggered*{0}Revision: {1}{2}{3}{0}BYOND Version: {4}.{5}",
Environment.NewLine,
commitInsert,
testmergeInsert,
remoteCommitInsert,
byondLock.Version.Major,
byondLock.Version.Minor),
cancellationToken)
.ConfigureAwait(false);
}
/// <summary>
@@ -523,9 +455,9 @@ namespace Tgstation.Server.Host.Components.Deployment
logger.LogDebug("Compile complete!");
}
catch (Exception e)
catch
{
await CleanupFailedCompile(job, e is OperationCanceledException, cancellationToken).ConfigureAwait(false);
await CleanupFailedCompile(job).ConfigureAwait(false);
throw;
}
}
@@ -547,174 +479,201 @@ namespace Tgstation.Server.Host.Components.Deployment
if (progressReporter == null)
throw new ArgumentNullException(nameof(progressReporter));
string repoOwner = null;
string repoName = null;
TimeSpan? averageSpan = null;
Models.RepositorySettings repositorySettings = null;
Models.DreamDaemonSettings ddSettings = null;
DreamMakerSettings dreamMakerSettings = null;
IRepository repo = null;
lock (deploymentLock)
{
if (deploying)
throw new JobException(ErrorCode.DreamMakerCompileJobInProgress);
deploying = true;
}
currentChatCallback = null;
currentDreamMakerOutput = null;
Models.CompileJob compileJob = null;
Models.RevisionInformation revInfo = null;
await databaseContextFactory.UseContext(
async databaseContext =>
{
averageSpan = await CalculateExpectedDeploymentTime(databaseContext, cancellationToken).ConfigureAwait(false);
ddSettings = await databaseContext
.DreamDaemonSettings
.Where(x => x.InstanceId == metadata.Id)
.Select(x => new Models.DreamDaemonSettings
{
StartupTimeout = x.StartupTimeout,
})
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (ddSettings == default)
throw new JobException(ErrorCode.InstanceMissingDreamDaemonSettings);
dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(cancellationToken).ConfigureAwait(false);
if (dreamMakerSettings == default)
throw new JobException(ErrorCode.InstanceMissingDreamMakerSettings);
repositorySettings = await databaseContext
.RepositorySettings
.Where(x => x.InstanceId == metadata.Id)
.Select(x => new Models.RepositorySettings
{
AccessToken = x.AccessToken,
ShowTestMergeCommitters = x.ShowTestMergeCommitters,
PushTestMergeCommits = x.PushTestMergeCommits,
PostTestMergeComment = x.PostTestMergeComment
})
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (repositorySettings == default)
throw new JobException(ErrorCode.InstanceMissingRepositorySettings);
repo = await repositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false);
try
{
if (repo == null)
throw new JobException(ErrorCode.RepoMissing);
if (repo.IsGitHubRepository)
{
repoOwner = repo.GitHubOwner;
repoName = repo.GitHubRepoName;
}
var repoSha = repo.Head;
revInfo = await databaseContext
.RevisionInformations
.Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id)
.Include(x => x.ActiveTestMerges)
.ThenInclude(x => x.TestMerge)
.ThenInclude(x => x.MergedBy)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (revInfo == default)
{
revInfo = new Models.RevisionInformation
{
CommitSha = repoSha,
OriginCommitSha = repoSha,
Instance = new Models.Instance
{
Id = metadata.Id
},
ActiveTestMerges = new List<RevInfoTestMerge>()
};
logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, repoSha);
databaseContext.Instances.Attach(revInfo.Instance);
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
}
}
catch
{
repo.Dispose();
throw;
}
})
.ConfigureAwait(false);
using (repo)
compileJob = await Compile(
revInfo,
dreamMakerSettings,
ddSettings.StartupTimeout.Value,
repo,
progressReporter,
averageSpan,
cancellationToken)
.ConfigureAwait(false);
var activeCompileJob = compileJobConsumer.LatestCompileJob();
try
{
string repoOwner = null;
string repoName = null;
TimeSpan? averageSpan = null;
Models.RepositorySettings repositorySettings = null;
Models.DreamDaemonSettings ddSettings = null;
DreamMakerSettings dreamMakerSettings = null;
IRepository repo = null;
Models.RevisionInformation revInfo = null;
await databaseContextFactory.UseContext(
async databaseContext =>
{
compileJob.Job = new Models.Job
{
Id = job.Id
};
compileJob.RevisionInformation = new Models.RevisionInformation
{
Id = revInfo.Id
};
averageSpan = await CalculateExpectedDeploymentTime(databaseContext, cancellationToken).ConfigureAwait(false);
databaseContext.Jobs.Attach(compileJob.Job);
databaseContext.RevisionInformations.Attach(compileJob.RevisionInformation);
databaseContext.CompileJobs.Add(compileJob);
ddSettings = await databaseContext
.DreamDaemonSettings
.Where(x => x.InstanceId == metadata.Id)
.Select(x => new Models.DreamDaemonSettings
{
StartupTimeout = x.StartupTimeout,
})
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (ddSettings == default)
throw new JobException(ErrorCode.InstanceMissingDreamDaemonSettings);
// The difficulty with compile jobs is they have a two part commit
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(cancellationToken).ConfigureAwait(false);
if (dreamMakerSettings == default)
throw new JobException(ErrorCode.InstanceMissingDreamMakerSettings);
repositorySettings = await databaseContext
.RepositorySettings
.Where(x => x.InstanceId == metadata.Id)
.Select(x => new Models.RepositorySettings
{
AccessToken = x.AccessToken,
ShowTestMergeCommitters = x.ShowTestMergeCommitters,
PushTestMergeCommits = x.PushTestMergeCommits,
PostTestMergeComment = x.PostTestMergeComment
})
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (repositorySettings == default)
throw new JobException(ErrorCode.InstanceMissingRepositorySettings);
repo = await repositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false);
try
{
await compileJobConsumer.LoadCompileJob(compileJob, cancellationToken).ConfigureAwait(false);
if (repo == null)
throw new JobException(ErrorCode.RepoMissing);
if (repo.IsGitHubRepository)
{
repoOwner = repo.GitHubOwner;
repoName = repo.GitHubRepoName;
}
var repoSha = repo.Head;
revInfo = await databaseContext
.RevisionInformations
.Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id)
.Include(x => x.ActiveTestMerges)
.ThenInclude(x => x.TestMerge)
.ThenInclude(x => x.MergedBy)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (revInfo == default)
{
revInfo = new Models.RevisionInformation
{
CommitSha = repoSha,
OriginCommitSha = repoSha,
Instance = new Models.Instance
{
Id = metadata.Id
},
ActiveTestMerges = new List<RevInfoTestMerge>()
};
logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, repoSha);
databaseContext.Instances.Attach(revInfo.Instance);
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
}
}
catch
{
// So we need to un-commit the compile job if the above throws
databaseContext.CompileJobs.Remove(compileJob);
await databaseContext.Save(default).ConfigureAwait(false);
repo.Dispose();
throw;
}
})
.ConfigureAwait(false);
var likelyPushedTestMergeCommit =
repositorySettings.PushTestMergeCommits.Value
&& repositorySettings.AccessToken != null
&& repositorySettings.AccessUser != null;
using (repo)
compileJob = await Compile(
revInfo,
dreamMakerSettings,
ddSettings.StartupTimeout.Value,
repo,
progressReporter,
averageSpan,
likelyPushedTestMergeCommit,
cancellationToken)
.ConfigureAwait(false);
var activeCompileJob = compileJobConsumer.LatestCompileJob();
try
{
await databaseContextFactory.UseContext(
async databaseContext =>
{
compileJob.Job = new Models.Job
{
Id = job.Id
};
compileJob.RevisionInformation = new Models.RevisionInformation
{
Id = revInfo.Id
};
databaseContext.Jobs.Attach(compileJob.Job);
databaseContext.RevisionInformations.Attach(compileJob.RevisionInformation);
databaseContext.CompileJobs.Add(compileJob);
// The difficulty with compile jobs is they have a two part commit
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
try
{
await compileJobConsumer.LoadCompileJob(compileJob, cancellationToken).ConfigureAwait(false);
}
catch
{
// So we need to un-commit the compile job if the above throws
databaseContext.CompileJobs.Remove(compileJob);
await databaseContext.Save(default).ConfigureAwait(false);
throw;
}
})
.ConfigureAwait(false);
}
catch
{
await CleanupFailedCompile(compileJob).ConfigureAwait(false);
throw;
}
var commentsTask = PostDeploymentComments(
revInfo,
activeCompileJob?.RevisionInformation,
repositorySettings,
repoOwner,
repoName);
var eventTask = eventConsumer.HandleEvent(EventType.DeploymentComplete, null, cancellationToken);
var chatTask = currentChatCallback(null, compileJob.Output);
currentChatCallback = null;
try
{
await Task.WhenAll(commentsTask, eventTask, chatTask).ConfigureAwait(false);
}
catch (Exception ex)
{
throw new JobException(ErrorCode.PostDeployFailure, ex);
}
}
catch (Exception ex)
{
await CleanupFailedCompile(compileJob, ex is OperationCanceledException, default).ConfigureAwait(false);
if (currentChatCallback != null)
await currentChatCallback(
ex.Message,
currentDreamMakerOutput)
.ConfigureAwait(false);
throw;
}
var commentsTask = PostDeploymentComments(
revInfo,
activeCompileJob?.RevisionInformation,
repositorySettings,
repoOwner,
repoName);
var eventTask = eventConsumer.HandleEvent(EventType.DeploymentComplete, null, cancellationToken);
var chatTask = chatManager.SendUpdateMessage(
String.Format(
CultureInfo.InvariantCulture,
"Deployment complete! Changes will be applied when DreamDaemon {0}.",
watchdog.Running ? "reboots" : "is launched"),
cancellationToken);
try
finally
{
await Task.WhenAll(commentsTask, eventTask, chatTask).ConfigureAwait(false);
}
catch (Exception ex)
{
throw new JobException(ErrorCode.PostDeployFailure, ex);
deploying = false;
}
}
#pragma warning restore CA1506
@@ -751,23 +710,32 @@ namespace Tgstation.Server.Host.Components.Deployment
return averageSpan;
}
async Task<Models.CompileJob> Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, Action<int> progressReporter, TimeSpan? estimatedDuration, CancellationToken cancellationToken)
async Task<Models.CompileJob> Compile(
Models.RevisionInformation revisionInformation,
Api.Models.DreamMaker dreamMakerSettings,
uint apiValidateTimeout,
IRepository repository,
Action<int> progressReporter,
TimeSpan? estimatedDuration,
bool localCommitExistsOnRemote,
CancellationToken cancellationToken)
{
logger.LogTrace("Begin Compile");
lock (compilingLock)
{
if (compiling)
throw new JobException(ErrorCode.DreamMakerCompileJobInProgress);
compiling = true;
}
using var progressCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var progressTask = estimatedDuration.HasValue ? ProgressTask(progressReporter, estimatedDuration.Value, cancellationToken) : Task.CompletedTask;
try
{
using var byondLock = await byond.UseExecutables(null, cancellationToken).ConfigureAwait(false);
await SendDeploymentMessage(revisionInformation, byondLock, cancellationToken).ConfigureAwait(false);
currentChatCallback = await chatManager.SendDeploymentMessage(
revisionInformation,
byondLock.Version,
DateTimeOffset.Now + estimatedDuration,
repository.GitHubOwner,
repository.GitHubRepoName,
localCommitExistsOnRemote,
cancellationToken)
.ConfigureAwait(false);
var job = new Models.CompileJob
{
@@ -788,7 +756,6 @@ namespace Tgstation.Server.Host.Components.Deployment
}
finally
{
compiling = false;
progressCts.Cancel();
await progressTask.ConfigureAwait(false);
}
@@ -262,7 +262,6 @@ namespace Tgstation.Server.Host.Components
eventConsumer,
chatManager,
processExecutor,
watchdog,
gitHubClientFactory,
dmbFactory,
repoManager,