File: CancelHelixJob.cs
Web Access
Project: src\src\Microsoft.DotNet.Helix\Sdk\Microsoft.DotNet.Helix.Sdk.csproj (Microsoft.DotNet.Helix.Sdk)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.Build.Framework;
using Microsoft.DotNet.Helix.Client;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
 
namespace Microsoft.DotNet.Helix.Sdk
{
    public class CancelHelixJobs : HelixTask
    {
        /// <summary>
        /// An array of Helix Jobs to cancel
        /// Jobs are expected to have a "CancellationToken" metadata item.
        /// If they lack this, we'll make an attempt to cancel using the Access Token from HelixTask, if supplied.
        /// Note token-based job cancellation requires the token to be the same as the user who started the job.
        /// </summary>
        [Required]
        public ITaskItem[] Jobs { get; set; }
 
        protected override async Task ExecuteCore(CancellationToken cancellationToken)
        {
            var api = HelixApi;
 
            Log.LogMessage($"Attempting to cancel {Jobs.Count()} Helix jobs");
 
            foreach (ITaskItem job in Jobs)
            {
                string correlationId = job.GetMetadata("Identity");
 
                Log.LogMessage(MessageImportance.High, $"Cancelling Helix Job {correlationId}");
 
                try
                {
                    // Any ITaskItem describing a job started in the same build will have this metadata.
                    // However, this standalone task is designed to be used to work around issues such as described in:
                    // https://developercommunity.visualstudio.com/t/ado-pipeline-timeouts-dont-cancel-the-same-as-when/1371617?from=email
                    // and ensure that anyone sufficiently motivated can still cancel Helix jobs after MSBuild is rapidly killed.
                    if (job.TryGetMetadata("HelixJobCancellationToken", out string helixCancellationToken))
                    {
                        await api.Job.CancelAsync(correlationId, helixCancellationToken, cancellationToken);
                        Log.LogMessage(MessageImportance.High, $"Successfully cancelled Helix Job {correlationId} via cancellation token.");
                    }
                    // Cancellation via token is preferred as these values are single-use (only work for one job) secrets and don't matter to leak.
                    else if (!string.IsNullOrEmpty(AccessToken))
                    {
                        Log.LogMessage(MessageImportance.High, "'HelixJobCancellationToken' metadata not supplied, will attempt to cancel using Access token. (Token must match user id that started the work)");
                        await api.Job.CancelAsync(correlationId, null, cancellationToken);
                        Log.LogMessage(MessageImportance.High, $"Successfully cancelled Helix Job {correlationId} via access token.");
                    }
                    else
                    {
                        Log.LogError($"Cannot cancel job '{job}'; please supply either the Job's cancellation token or the job creator's access token");
                    }
                }
                catch (RestApiException e) when (e.Response.Status == 304)
                {
                    // Helix Cancel API's "Not Modified" == Already cancelled; not really an error case
                    Log.LogMessage($"Job '{correlationId}' was already cancelled.");
                }
                catch (RestApiException e) when (e.Response.Status == 404)
                {
                    // Not found can indicate calling very close to job creation or accidentally mixing and matching anonymous/authenticated.
                    // Try to be helpful with an error message.
                    Log.LogError($"Job '{correlationId}' was not found. Check if you are mixing and matching authenticated and anonymous access, or accessing instantly after job creation");
                }
                catch (Exception toLog)
                {
                    Log.LogErrorFromException(toLog, false);
                }
            }
 
            if (!Log.HasLoggedErrors)
            {
                Log.LogMessage(MessageImportance.High, $"Successfully cancelled {Jobs.Count()} Helix jobs");
            }
        }
    }
}