Creación automatizada de proyectos en Visual Studio Team Services

Una de las cosas que más me gustan por ejemplo de GIT, es la facilidad con la cual añades un origen externo (por ejemplo desde Team Services, Azure o GitHub), bajas el código y comienzas a trabajar de manera segura sobre tu solución; y todo sin salirte de una simple consola de comandos.

Sin embargo una de las piezas faltantes que no podía ejecutar desde la consola, es la creación del repositorio o proyecto como tal en Team Services. Entonces tenía que hacer una pausa, ir al portal, loguearme si no lo estaba, crear el proyecto, buscar la url del git y luego sí conectar todo con mi cliente de desarrollo.

Obviamente Team Services posee una API Rest para poder ejecutar operaciones programáticamente. Así que decidí investigar como hacer una aplicación de consola que me permitiera crear el repositorio en Team Services de manera automática sin entrar al portal.

Una forma muy sencilla de poder acceder al API de Team Services es a través de los “Personal Access Token” (PAT). Si tienes una cuenta de Team Services, lo puedes obtener desde el portal (solo hay que hacerlo una vez). Entrando a la parte de seguridad dentro de tu cuenta:

image

 

Allí de una vez encontrarás el acceso a la administración de los PAT relativos a tu cuenta de Team Services. Para un mejor control recomiendo generar un PAT único para tu aplicación de creación de proyectos. Puedes escoger qué permisos otorgará ese PAT sobre los servicios de Team Services y por cuánto tiempo será válido. La idea es que luego uses el PAT para hacer tus llamados a la API sin necesidad de un usuario y contraseña o tal vez de doble autenticación.

image

Quise desarrollar este proyecto usando .NETCore, (si no estás familiarizado con .NETCore, puedes ver este corto video que creé para mostrar de qué se trata muy rápidamente), de manera que cuando esté en Linux o acaso en MacOS, igual pueda usar la herramienta. Es un proyecto que hace uso extensivo del archivo de settings para .NETCore, que como ya es estándar, he llamado appsettings.json:

   {   "domains": {
     "warnov": "YOUR PAT",
      "sample": "SampleAccountPATlaxs3zhqf74rcflkxqhufbmhnhpmma"   
     },
   "templates": {
      "scrum": "6B724908-EF14-45CF-84F8-768B5384DA45",
      "agile": "ADCC42AB-9882-485E-A3ED-7678F01F66BC", 
     "CMMI": "27450541-8E31-4150-9947-DC59F998FC01"   
     },
   "sourceControl": [ "Git", "Tfvc" ],  
    "defaults": { 
     "sourceControlProvider": "git",      "template": "6B724908-EF14-45CF-84F8-768B5384DA45"       },    "apiPath": "https://{0}.visualstudio.com/_apis/projects?api-version=2.2"
}

 

El trabajo con este tipo de archivos en .NETCore es algo especial comparado a como hacíamos antes con archivos como el Web.Config o App.Config. Empezando porque ya no son archivos XML sino JSON. Para una guía detallada para aprender a manejar estos tipos de archivos, consulta este post que escribí al respecto.

En primera instancia encontramos una sección de dominios, donde especificamos el PAT de cada uno de ellos. Esto quiere decir que por ejemplo en el código anterior, puedo trabajar con Team Services en warnov.visualstudio.com y sample.visualstudio.com, pues se supone que allí he especificado los PAT válidos para cada uno de ellos.

Luego tenemos los templates; como lo puedes ver en las llaves, hacen referencia a si vamos a trabajar con scrum, agile o CMMI el ciclo de vida de nuestro proyecto de software. Generalmente escojo scrum.

Después vienen las opciones que tendremos para administrar el código fuente. Hoy en día Team Services ofrece Git y Tfvc (Team Foundation Version Control). Últimamente estoy optando por usar más git por lo simplista de su acercamiento.

También creé una sección de valores por defecto tanto para el proveedor de source control y el template a usar; esto para que mi aplicación pueda funcionar sin necesidad de que yo especifique todos los parámetros cuando la llame.

Para finalizar, especifico el formato de la ruta del API a usar. Sucede que ésta API está en constante evolución y me ha pasado que la URL cambia ligeramente. O acaso quieras usar otra versión de la API. En ese caso, basta con modificar este formato. Observa el {0}: éste va a ser reemplazado con uno de los nombres de dominios que has especificado arriba y que será aquel que escojas para trabajar.

Ahora sí, let’s get fun!

 

 if (args.Length > 0)
{
    workingDomain = args[0];
    workingProject = args[1];
    description = args[2];
    var defaults = Config.GetSection("defaults");
    sourceControlProvider = defaults["sourceControlProvider"];
    templateId = defaults["template"];
}

Sencillamente evalúo si he pasado argumentos a la herramienta. En el primero vendría el dominio… por ejemplo warnov si quiero trabajar con warnov.visualstudio.com. Luego el nombre del proyecto. Por ejemplo: Picas y Fijas on the Cloud y finalmente la descripción del proyecto: “Maravilloso y revolucionario juego interactivo”. Como se aprecia, los otros valores son extraídos de la sección de defaults que definí en mi appsettings. de hecho, esto les da la idea de cómo se trabaja con ese archivo.

Si no paso parámetros, los pido de manera interactiva:

 else
{
    //Domain
    workingDomain = GetDomain();
    //Project Name
    Console.WriteLine("Project Name: ");
    workingProject = Console.ReadLine();
    //Project Description
    Console.WriteLine("Project Description: ");
    description = Console.ReadLine();
 
    //SOURCE CONTROL           
    sourceControlProvider = GetSourceControlProvider();
 
    TEMPLATE ID
    templateId = GetTemplateId();                 }

y eso es todo. Luego pedimos los otros valores requeridos para la conexión, como el PAT; armamos el mensaje a enviar al API, lo enviamos usando POST y luego con el resultado mostramos un mensaje al final de la ejecución del programa.

 //PAT
var pat = domainsSection[workingDomain];
//Message Body
var messageBody = GetMessageBody(workingProject, description, sourceControlProvider, templateId);
//PostExecution
var postResult = ExecutePost(workingDomain, pat, messageBody);
Console.WriteLine($"{postResult}\n\nPress Enter to Finish...");
Console.ReadLine();

Mensaje a enviar:

 ProjectCreator body = new ProjectCreator(workingProject, description, sourceControlProvider, templateId);
var jsonBody = JsonConvert.SerializeObject(body);
return new StringContent(jsonBody, Encoding.UTF8, "application/json");

Observa que hay una clase modelo que se llama ProjectCreator. Desarrollé ésta clase a partir de la documentación del API, usando la metodología que he descrito en este post. Es una metodología que ayuda mucho a poder conectarse a todas las APIs modernas basadas en REST y JSON.

Pues bien, una ves inicializo ésta clase con los valores requeridos para la creación de mi proyecto en Team Services, la convierto a un JSON que luego paso a un StringContent que es el objeto provisto por  el framework que prepara los datos y los deja listos para ser incluidos en el body de un mensaje post que incluye data en formato texto para ser enviada al servidor.

Finalmente pasamos al envío del mensaje:

 private static string ExecutePost(string workingDomain, string pat, StringContent messageBody)
{
    string responseBody=string.Empty;
    using (HttpClient client = new HttpClient())
    {
        string _credentials = Convert.ToBase64String(System.Text.ASCIIEncoding.ASCII.GetBytes(string.Format("{0}:{1}", "", pat)));
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", _credentials);
        var url = Config["apiPath"];
         var request = new HttpRequestMessage(HttpMethod.Post,
            string.Format(url, workingDomain))
        {
            Content = messageBody
        };                       try
        {
            var response = client.SendAsync(request).Result;
            response.EnsureSuccessStatusCode();
            var code = response.StatusCode;
            if (response.IsSuccessStatusCode)
            {
                responseBody = $"{code}: {response.Content.ReadAsStringAsync().Result}";
            }                                                          }
        catch (Exception exc)
        {
            responseBody = $"ERROR: {exc.Message}. {exc.InnerException?.Message}";
        }
    }
    return responseBody;
}

En primera medida ajustamos las credenciales de acceso al API, encodificando el PAT que hemos obtenido. Luego ajustamos el header de autorización con esta data. También indicamos que el API maneja mensajes JSON.

Después armamos la url de envío de nuestro request y adicionamos el mensaje que armamos previamente.

Finalmente enviamos el request y le pedimos al sistema que si no nos responden con éxito lance una excepión, a través de response.EnsureSuccessStatusCode();

Observa que la respuesta inicial no va a ser un 200, porque la operación no finaliza en bien recibimos la respuesta. La creación de un proyecto toma algún tiempo así que lo que es retornado por el servidor de Team Services es el código 202 que nos indica que todo está bien, pero que debemos esperar a que la operación termine (cosa que por lo general podemos hacer sin tomar acción especial distinta a esperar algunos segundos). También se nos retorna una URL de seguimiento del estado del request por si queremos automatizar la herramienta para que ella misma cree el repositorio local una vez haya quedado creado en la nube. Pero eso haría parte de otro post.

Así que después solo resta inicializar nuestro repositorio git, y agregarle el remote con dirección similar a esta:
https://[yourdomain].visualstudio.com/DefaultCollection/_git/[yourprojectname]</STRONG]

y voilá… habremos podido crear todo lo necesario para nuestro ambiente de desarrollo local soportado por Team Services, sin necesidad de salir de la consola!

Todo el código de esta solución lo pueden encontrar en mi Github. Pueden agregarle más funcionalidades basados en el API de Team Services o modificar su comportamiento. Estaré atento a sus push requests!

github-512