A pleasant walk through computing

Comment for me? Send an email. I might even update the post!

TFS/Azure DevOps: Building and Releasing Git Branches, A Simple Example

Contents

The Situation

The development teams run on a two-week release cycle. The final three days of the cycle they are in code freeze. Code is deployed to two environments: Test and Production.

During code freeze:

  1. At the start, all repos are built and released to Test. This is the production code.
  2. Newly developed code must be committed to the repository and built, but it must not be released to Test.
  3. At the end, all production code is deployed to Production, and newly developed code is deployed to Test.

Finally, the teams want to have just one build and one deployement definition for each solution.

Implementation

Git

Git is configured with two long-lived branches: master and development.

  • Developers always work against the development branch, pushing their local, short-lived feature branches.
  • At code freeze, development is merged into master.
  • If there are emergency production deployments, ideally they're made first to deployment and then to master. If they're made directly to master then the changes need to be merged into development (or, ideally, development is rebased onto master)

TFS Build

The build definition is configured with Triggers, specifically Branch filters for both master and development branches. Continuous Integration is enabled.

This allows commits to both development and master branches to trigger the build.

TFS Release

The deployment definition is configured in the following way:

Pipeline

Artifacts
There's a single artifact with a Source type of Build. The Source is the build definition.

Continuous deployment is enabled, and both branches are allowed.

Environments
Both environments use the After release trigger.

Test Environment
The artifact filters include both branches

In the sample, a Copy Files task is used to move the binaries to their release folder. While not required for the solution, it demonstrates using variables to create release subfolders. In this case, folders for branch and build number are created. Also note the Overwrite and Flatten Folders checkboxes.

Production Environment
The differences between this and the Test environment are

  • Only the master branch is included in the artifact filters
  • Approvers are added
  • Only the latest build is deployed upon approval



Code Freeze

What has to happen during the code freeze?

Development branch commits don't deploy. That's it.

There are at least two ways to prevent development branch deployments during code freeze.

  1. Add a build tag using a custom task that checks for the presense of a particular file. A custom build task would be used for this.

    Sorry, I don't have an example of this. But I've seen it done in a real-world environment.

  2. Use a Gated release. For example, check a web service endpoint. If it fails to return, you're in code freeze and the gate fails.

Recommendation: Stick with build tags

The disadvantage to tagging builds is that the custom task needs to be added to each build definition. The advantage is that the tag is part of the artifact trigger.

The disadvantage to gates is that there's a required timeout period of at-minimum 6 minutes before the gate fails. The advantage is that build definitions don't need to create tags.

REST API Gate Sample

Web API Endpoint Sample

   public class InCodeFreezeController : ApiController
    {
        public IHttpActionResult Get()
        {
            //IsInCodeFreeze checks some condition. Could be the same file-existence
            //check that's used for tagging today.
            //Not ideal, but simpler for TFS Gate to just fail the request
            if (IsInCodeFreeze)
            {
                return BadRequest();
            }
            return Ok();
        }
    }

Who Are You Coding For?

The Lesson

Consider this brief, hypothetical exchange between two developers, maybe in a meeting where others are present.

Developer 1: "...so, I solved the problem this way. This causes e.Data to overwrite the previous line."

bool overwrite = e.Data.Contains(".....");

WriteLog(e.Data, overwrite);

Developer 2: "You know you could just include the condition in the WriteLog call. You don't need to go to the trouble of setting a variable."

WriteLog(e.Data, e.Data.Contains("....."));

Developer 1: "Sure. So, why did I?"

Developer 2: "Huh?"

Developer 1: "Why did I use a variable?"

Developer 2: "I don't know."

Developer 1: "Because I'm not coding for me today. I'm coding for other developers, and my future self. I want it to be as easy for them as possible to understand what this code is doing."

Why I wrote this

My grandfather was once desribed as a bridge-builder. Even if he were (metaphorically) crossing a river just once, he'd build a bridge for whoever came after him.

Think about this as you make coding, architectural, and documentation decisions. What you do will affect someone in the future. Are you focused on finishing now, as fast as possible? Or crafting your code so that someone thanks you later on.

Who are you coding for?

The ViewService Pattern: Especially Good For Windows Forms

Windows Form development is still happening, is fast, and is reasonably easy to understand how to get started with. It's event-driven and hasn't changed much since Visual Basic. You drag controls onto a canvas, double-click the controls to open their event methods, and write what's supposed to happen.

Thus, Frankenstein's monster was born.

I'm taming the monster with a pattern I'm calling the ViewService. I won't create a Gang of Four class diagram, but basically a ViewService is a way of separating the code that manages ViewModels, and is a useful approach in Windows Forms. It's directly analagous to domain services and models.

How often have you written or maintained code that looks like this? (note this is pseudocoding, not actual code.)

class MainForm
{
    void buttonLoadData(object sender, EventArgs e)
    {
        var cn = new Connection(_connString);
        var cmd = new Command(cn, "select a.*, b.* from Customer a join Order b on a.CustomerId = b.CustomerId");
        var reader = cmd.ReadResults();
        until (reader.Eof)
        {
            var co = ConvertReaderToCustomerOrder(reader.Read());
            grid1.Row.Add(new Row(co.Name, co.Zip);
            AddOrdersToGridRow(grid1.Rows[grid1.Rows.Length-1], co)
            if (co.Type = 1) { grid1.Rows[grid1.Rows.Length-1].BackgroundColor = Blue;}
            else if (co.Type = 3) { grid1.Rows[grid1.Rows.Length-1].BackgroundColor = Orange;}
        }
        
    }
    
    void AddOrdersToGridRows(Row row, CustomerOrder order, Connection cn)
    {
        foreach (var orderitem in order.Orders)
        {
            var cmd = new Command();
            cmd.Connection = cn;
            cmd.CommandText = String.Format ("select * from lineitems where OrderId = %1",  orderitem.OrderId);
            var items = ToLineItems(cmd.ExecuteQuery());
            row["Items"] = AddItemsToGrid(items, row);
            if (order.Type = 3 and items.Count() > 15) {row.BackgroundColor = Red;}
        }
        CheckIfMoreOrdersHaveArrivedAndPrintThem();
        
    }
    
    void buttonRefreshData(object sender, EventArgs e)
    {
        buttonLoadData(sender, e);
    }
    
}

The problems with the above code could occupy us for awhile, and they add up to one word: complication.

  • Events are doing too many things
  • Events are called directly
  • Tight coupling
  • Inconsistent naming and coding style

Here's how I recommend clearing up this kind of code by applying the ViewService pattern.

  1. Group form events together
  2. Group together methods that only apply to this form
  3. Group methods that could apply to a replacement form together, potentially into a service
  4. Form events present data, or call a method to preserve data

Let's apply these steps to the above.

The code below is pretty sparse and incomplete. The goal is to give you the idea of what to do, not provide a full-fledged implementation.

Group form events and methods

This is organizational, and clarifies what the user is doing vs what the developer is doing. (Methods collapsed for clarity.)


+ void AddOrdersToGridRows(Row row, CustomerOrder order, Connection cn)...

#region "Control Events"

+ void buttonLoadData(object sender, EventArgs e)...
+ void buttonRefreshData(object sender, EventArgs e)...

#endregion

Better methods and names, and events call custom methods instead of being treated as custom methods

class MainForm
{
    List<CustomerOrderView> _customerOrders = new List<CustomerOrderView>();
    
    + void GetData()...
    + void LoadControls()
    
    void buttonLoadData(object sender, EventArgs e)
    {
        GetData();
        LoadControls();
    }
    void buttonCancel(object sender, EventArgs e)
    {
        LoadControls();
    }
}

Already, we're gaining clarity.

Separate the data calls into a ViewService

Imagine the ViewService is going to be resued in a web application. That means it doesn't accept or process form controls, and is UI agnostic.

This is the same Dependency Injection pattern used in web applications. The difference is that CustomerOrderViewService isn't a domain service, it's specific to this view of the data.


class MainForm 
{
    ICustomerOrderViewService _customerOrderViewService = null;
    List<CustomerOrderView> _customerOrders = new List<CustomerOrderView>();

    
    + void MainForm(ICustomerOrderViewService)...
    
    void GetData()
    {
        _customerOrders = _customerOrderViewService.Get(txtCustomerId.Text);
    }
    
    void LoadControls()
    {
        if (_customerOrders == null) { GetData(); }
        gridOrders.DataSource = _customerOrders;
    }

}

//These two classes would be in separate files, and *could* be in a separate namespace
//to emphasize the decoupling.

class CustomerOrderViewService: ICustomerOrderViewService
{
    ICustomerOrderService _customerOrderService = null;
    
    + public CustomerOrderViewService(ICustomerOrderService _customerOrderService)...
    

    public List<CustomerOrderView> Get(string customerId)
    {
        //This is the call to the *domain service*. It might call the database directly, or might in turn call a web api.
        
        //Returns type CustomerOrder
        var customerOrders = _customerOrderService.GetOrdersByCustomer(string customerId);
        
        //Mapping
        return customerOrders.Select(a => a.ToCustomerOrderView());
    }
    
}

class static CustomerOrderViewServiceHelpers
{
    public static ToCustomerOrderView (this CustomerOrder customerOrder)
    {
        return new CustomerOrderView()
        {
            Name = customerOrder.Name,
            CustomerType = customerOrder.CustomerType,
            etc....
        }
    }
}

The Happy Wrap Up

By applying the ViewService pattern, we can separate Windows Form code into cleaner areas of concern, making our code clear, testable, maintainable and replaceable.