Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
18 changes: 17 additions & 1 deletion doc/rule-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ $"Hello { self.WorkItemType } #{ self.Id } - { self.Title }!"
## Auto-close parent

This is more similar to classic TFS Aggregator.
It move a parent work item to Closed state, if all children are closed.
It moves a parent work item to Closed state, if all children are closed.
The major difference is the navigation: `Parent` and `Children` properties do not returns work items but relation. You have to explicitly query Azure DevOps to retrieve the referenced work items.

```
Expand All @@ -34,6 +34,22 @@ if (parent != null)
return message;
```

## Work item update

Check if a work item was updated and execute actions based on the changes, e.g. if work item Title was updated.

```
if (selfChanges.Fields.ContainsKey("System.Title"))
{
var titleUpdate = selfChanges.Fields["System.Title"];
return $"Title was changed from '{titleUpdate.OldValue}' to '{titleUpdate.NewValue}'";
}
else
{
return "Title was not updated";
}
```

## History

`PreviousRevision` is different because retrieves a read-only version of the work item.
Expand Down
54 changes: 54 additions & 0 deletions doc/rule-language.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,60 @@ in recycle bin.
Returns the number of attached files.


# WorkItem Changes
If the rule was triggered by the `workitem.updated` event, the changes
which were made to the WorkItem object, are contained in the `selfChanges` variable.

## Fields
Data fields of the work item update.

`int Id` Read-only.
The unique identifier of the _Update_.
Each change leads to an increased update id, but not necessarily to an updated revision number.
Changing only relations, without changing any other information does not increase revision number.

`int WorkItemId` Read-only.
The unique identifier of the _work item_.

`int Rev` Read-only.
The revision number of work item update.

`IdentityRef RevisedBy` Read-only.
The Identity of the team member who updated the work item.

`DateTime RevisedDate` Read-only.
The date and time when the work item updates revision date.

`WorkItemFieldUpdate Fields[string field]` Read-only.
Access to the list of updated fields.
Must use reference name, like _System.Title_, instead of language specific, like _Titolo_, _Titel_ or _Title_.

`WorkItemRelationUpdates Relations` Read-only.
Returns the information about updated relations

## WorkItemFieldUpdate
Updated Field Information containing old and new value.

`object OldValue` Read-only.
Returns the previous value of the field or `null`

`object NewValue` Read-only.
Returns the new value of the field


## WorkItemRelationUpdates
Groups the changes of the relations

`ICollection<WorkItemRelation> Added` Read-only.
Returns the added relations as `WorkItemRelation`.

`ICollection<WorkItemRelation> Removed` Read-only.
Returns the removed relations as `WorkItemRelation`.

`ICollection<WorkItemRelation> Updated` Read-only.
Returns the updated relations as `WorkItemRelation`.



# WorkItemStore Object
The WorkItemStore object allows retrieval, creation and removal of work items.
Expand Down
61 changes: 37 additions & 24 deletions src/aggregator-function/AzureFunctionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using aggregator.Engine;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.ServiceHooks.WebApi;
using Newtonsoft.Json.Linq;
using ExecutionContext = Microsoft.Azure.WebJobs.ExecutionContext;
Expand Down Expand Up @@ -60,10 +62,9 @@ public async Task<HttpResponseMessage> RunAsync(HttpRequestMessage req, Cancella
}

var data = JsonConvert.DeserializeObject<WebHookEvent>(jsonContent);
string eventType = data.EventType;

// sanity check
if (!DevOpsEvents.IsValidEvent(eventType)
if (!DevOpsEvents.IsValidEvent(data.EventType)
|| data.PublisherId != DevOpsEvents.PublisherId)
{
return req.CreateResponse(HttpStatusCode.BadRequest, new
Expand All @@ -72,29 +73,12 @@ public async Task<HttpResponseMessage> RunAsync(HttpRequestMessage req, Cancella
});
}

var resourceObject = data.Resource as JObject;
WorkItem workItem;
if (ServiceHooksEventTypeConstants.WorkItemUpdated == eventType)
var eventContext = CreateContextFromEvent(data);
if (eventContext.IsTestEvent())
{
workItem = resourceObject.GetValue("revision").ToObject<WorkItem>();
}
else
{
workItem = resourceObject.ToObject<WorkItem>();
return RespondToTestEventMessage(req, aggregatorVersion);
}

if (workItem.Url.StartsWith("http://fabrikam-fiber-inc.visualstudio.com/DefaultCollection/"))
{
var resp = req.CreateResponse(HttpStatusCode.OK, new
{
message = $"Hello from Aggregator v{aggregatorVersion} executing rule '{_context.FunctionName}'"
});
resp.Headers.Add("X-Aggregator-Version", aggregatorVersion);
resp.Headers.Add("X-Aggregator-Rule", _context.FunctionName);
return resp;
}
string collectionUrl = data.ResourceContainers["collection"].BaseUrl;

var config = new ConfigurationBuilder()
.SetBasePath(_context.FunctionAppDirectory)
.AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
Expand All @@ -107,8 +91,7 @@ public async Task<HttpResponseMessage> RunAsync(HttpRequestMessage req, Cancella
var wrapper = new RuleWrapper(configuration, logger, _context.FunctionName, _context.FunctionDirectory);
try
{
Guid teamProjectId = data.ResourceContainers["project"].Id;
string execResult = await wrapper.ExecuteAsync(new Uri(collectionUrl), teamProjectId, workItem, cancellationToken);
string execResult = await wrapper.ExecuteAsync(eventContext, cancellationToken);

if (string.IsNullOrEmpty(execResult))
{
Expand Down Expand Up @@ -138,6 +121,36 @@ public async Task<HttpResponseMessage> RunAsync(HttpRequestMessage req, Cancella
}
}

private HttpResponseMessage RespondToTestEventMessage(HttpRequestMessage req, string aggregatorVersion)
{
var resp = req.CreateResponse(HttpStatusCode.OK, new
{
message = $"Hello from Aggregator v{aggregatorVersion} executing rule '{_context.FunctionName}'"
});
resp.Headers.Add("X-Aggregator-Version", aggregatorVersion);
resp.Headers.Add("X-Aggregator-Rule", _context.FunctionName);
return resp;
}

private static WorkItemEventContext CreateContextFromEvent(WebHookEvent eventData)
{
var collectionUrl = eventData.ResourceContainers.GetValueOrDefault("collection")?.BaseUrl;
var teamProjectId = eventData.ResourceContainers.GetValueOrDefault("project")?.Id ?? Guid.Empty;

var resourceObject = eventData.Resource as JObject;
if (ServiceHooksEventTypeConstants.WorkItemUpdated == eventData.EventType)
{
var workItem = resourceObject.GetValue("revision").ToObject<WorkItem>();
var workItemUpdate = resourceObject.ToObject<WorkItemUpdate>();
return new WorkItemEventContext(teamProjectId, new Uri(collectionUrl), workItem, workItemUpdate);
}
else
{
var workItem = resourceObject.ToObject<WorkItem>();
return new WorkItemEventContext(teamProjectId, new Uri(collectionUrl), workItem);
}
}

private static T GetCustomAttribute<T>()
where T : Attribute
{
Expand Down
9 changes: 4 additions & 5 deletions src/aggregator-function/RuleWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using aggregator.Engine;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.ServiceHooks.WebApi;
using Microsoft.VisualStudio.Services.WebApi;

namespace aggregator
Expand All @@ -29,7 +28,7 @@ public RuleWrapper(AggregatorConfiguration configuration, IAggregatorLogger logg
this.functionDirectory = functionDirectory;
}

internal async Task<string> ExecuteAsync(Uri collectionUri, Guid teamProjectId, WorkItem workItem, CancellationToken cancellationToken)
internal async Task<string> ExecuteAsync(WorkItemEventContext eventContext, CancellationToken cancellationToken)
{
logger.WriteVerbose($"Connecting to Azure DevOps using {configuration.DevOpsTokenType}...");
var clientCredentials = default(VssCredentials);
Expand All @@ -46,7 +45,7 @@ internal async Task<string> ExecuteAsync(Uri collectionUri, Guid teamProjectId,
cancellationToken.ThrowIfCancellationRequested();

// TODO improve from https://github.com/Microsoft/vsts-work-item-migrator
using (var devops = new VssConnection(collectionUri, clientCredentials))
using (var devops = new VssConnection(eventContext.CollectionUri, clientCredentials))
{
await devops.ConnectAsync(cancellationToken);
logger.WriteInfo($"Connected to Azure DevOps");
Expand All @@ -69,7 +68,7 @@ internal async Task<string> ExecuteAsync(Uri collectionUri, Guid teamProjectId,

var engine = new Engine.RuleEngine(logger, ruleCode, configuration.SaveMode, configuration.DryRun);

return await engine.ExecuteAsync(teamProjectId, workItem, witClient, cancellationToken);
return await engine.ExecuteAsync(eventContext.ProjectId, eventContext.WorkItemPayload, witClient, cancellationToken);
}
}
}
Expand Down
14 changes: 0 additions & 14 deletions src/aggregator-ruleng/BatchRequest.cs

This file was deleted.

1 change: 1 addition & 0 deletions src/aggregator-ruleng/Globals.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace aggregator.Engine
public class Globals
{
public WorkItemWrapper self;
public WorkItemUpdateWrapper selfChanges;
public WorkItemStore store;
public IAggregatorLogger logger;
}
Expand Down
8 changes: 5 additions & 3 deletions src/aggregator-ruleng/RuleEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ public RuleEngine(IAggregatorLogger logger, string[] ruleCode, SaveMode mode, bo
.WithEmitDebugInformation(true)
.WithReferences(references)
// Add namespaces
.WithImports(imports)
;
.WithImports(imports);

this.roslynScript = CSharpScript.Create<string>(
code: directives.GetRuleCode(),
Expand Down Expand Up @@ -107,21 +106,24 @@ private static IEnumerable<string> GetImports(DirectivesParser directives)
public EngineState State { get; private set; }
public bool DryRun { get; }

public async Task<string> ExecuteAsync(Guid projectId, WorkItem workItem, WorkItemTrackingHttpClient witClient, CancellationToken cancellationToken)
public async Task<string> ExecuteAsync(Guid projectId, WorkItemData workItemPayload, WorkItemTrackingHttpClient witClient, CancellationToken cancellationToken)
{
if (State == EngineState.Error)
{
return string.Empty;
}

var workItem = workItemPayload.WorkItem;
var context = new EngineContext(witClient, projectId, workItem.GetTeamProject(), logger);
var store = new WorkItemStore(context, workItem);
var self = store.GetWorkItem(workItem.Id.Value);
var selfChanges = new WorkItemUpdateWrapper(workItemPayload.WorkItemUpdate);
logger.WriteInfo($"Initial WorkItem {self.Id} retrieved from {witClient.BaseAddress}");

var globals = new Globals
{
self = self,
selfChanges = selfChanges,
store = store,
logger = logger
};
Expand Down
21 changes: 0 additions & 21 deletions src/aggregator-ruleng/WorkItemBatchPostResponse.cs

This file was deleted.

21 changes: 21 additions & 0 deletions src/aggregator-ruleng/WorkItemData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;


namespace aggregator.Engine
{
public class WorkItemData
{

public WorkItemData(WorkItem workItem, WorkItemUpdate workItemUpdate = null)
{
WorkItem = workItem;
WorkItemUpdate = workItemUpdate ?? new WorkItemUpdate();
}

public WorkItem WorkItem { get; }

public WorkItemUpdate WorkItemUpdate { get; }

public static implicit operator WorkItemData(WorkItem workItem) => new WorkItemData(workItem);
}
}
31 changes: 31 additions & 0 deletions src/aggregator-ruleng/WorkItemEventContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;

namespace aggregator.Engine
{

public class WorkItemEventContext
{
public WorkItemEventContext(Guid projectId, Uri collectionUri, WorkItem workItem, WorkItemUpdate workItemUpdate = null)
{
ProjectId = projectId;
CollectionUri = collectionUri;
WorkItemPayload = new WorkItemData(workItem, workItemUpdate);
}

public WorkItemData WorkItemPayload { get; }
public Guid ProjectId { get; }
public Uri CollectionUri { get; }
}

public static class WorkItemEventContextExtension
{
public static bool IsTestEvent(this WorkItemEventContext eventContext)
{
const string TEST_EVENT_COLLECTION_URL = "http://fabrikam-fiber-inc.visualstudio.com/DefaultCollection/";

var workItem = eventContext.WorkItemPayload.WorkItem;
return workItem.Url.StartsWith(TEST_EVENT_COLLECTION_URL, StringComparison.OrdinalIgnoreCase);
}
}
}
Loading