Integrare XmlPreprocess nella build

Questa breve nota descrive come integrare un tool quale XmlPreprocess nella build di Team Foundation Server 2010 o 2012. L’esempio in sé ha una sua utilità spicciola — i file di configurazione nello share di Drop vengono predisposti per un ambiente target — ma illustra come, in generale, integrare il lancio di un tool eseguibile all’interno del processo di build.

Cos’è XmlPreprocess

Nelle parole dell’autore “XmlPreprocess is a command-line utility that can modify annotated XML files much like a code preprocessor. It is useful for deploying configuration files to different environments making substitutions such as connection strings”. Si può scaricare, inclusi sorgenti e documentazione da http://xmlpreprocess.codeplex.com/.

 

Figura 1 – Funzionamento di XmlPreprocess

Come si vede in Figura 1, il tool legge da un foglio Excel i valori di configurazione che sostituisce nel file che riceve in input.

In questo esempio utilizziamo il file Excel, ma il tool è in grado di usare altre fonti come, ad esempio, un database.

Disegno della soluzione

L’esempio assume la presenza di due file essenziali: l’eseguibile XmlPreprocess.exe e il file SettingsSpreadsheet.xml che contiene i valori da sostituire.

Il primo, l’eseguibile, deve trovarsi nella stessa cartella del workflow di build (build process template).

Il secondo è opportuno venga conservato in una cartella speciale con ristretti permessi di sicurezza, in modo che possa accedervi solo il servizio di build e il personale che ha in gestione i sistemi e che ha quindi la responsabilità delle configurazioni e delle password.

Il tool viene lanciato al termine della compilazione e dello unit test, quando tutti gli artefatti sono giunti nello share di Drop: in questo modo i file di configurazione ivi contenuti sono pronti per essere installati in un ambiente target senza ulteriori passaggi.

Personalizzazione del workflow di build

Il workflow personalizzato è un clone del Default template.

Abbiamo aggiunto due argomenti supplementari (v. Figura 2): il primo, TargetEnvironment, indica da quale ambiente, censito nel file Excel, prenderemo i valori per la sostituzione; il secondo, SettingsSpreadsheetPath, è il percorso completo del file Excel, espresso come version control path, ossia quelli che iniziano con $ .

GVTFS12_2013-02-07_00001 
GVTFS12_2013-02-07_00002GVTFS12_2013-02-07_00003

Figura 2 – Argomenti supplementari del workflow

È stata aggiunta una sezione (sequenceActivity) nell’ambito (scope) “Run on Agent” come ultimo passo, dopo che gli eseguibili sono stati prodotti nella cartella di Drop; la sezione si chiama “*** Apply XmlPP” (gli asterischi aiutano a identificare la personalizzazione.

XmlPreprocessWorkflow

 

Figura 3 – Personalizzazione del workflow di build

La sequenza di azioni è:

  1. calcolo dove si trova l’eseguibile di XmlPreprocess — la proprietà ServerPath ci fornisce il percorso, in termini di version control, del template di build che viene convertito in un percorso locale mediante ConvertWorkspaceItem; da questo si ricava la cartella contenente e quindi il percorso locale assoluto dell’eseguibile
  2. scarico dal version control il file Excel — con l’activity DownloadFiles lo portiamo in locale nella stessa cartella dove si trova
  3. cerco tutti i file .config
  4. richiamo XmlPreprocess per ciascun file

Il sorgente è riportato nel Listato 1.

 <Sequence DisplayName="*** Apply XmlPP">  <Sequence.Variables>    <Variable x:TypeArguments="x:String" Name="localPathToXmlPP" />    <Variable x:TypeArguments="scg:IEnumerable(x:String)" Name="configFiles" />    <Variable x:TypeArguments="x:String" Name="localPathToSpreadsheet" />  </Sequence.Variables>  <mtbwa:ConvertWorkspaceItem DisplayName="Convert BuildTemplate path to local" Input="[BuildDetail.BuildDefinition.Process.ServerPath]" Result="[localBuildTemplatePath]" Workspace="[Workspace]" />  <Assign DisplayName="Assign localPathToXmlPP">    <Assign.To>      <OutArgument x:TypeArguments="x:String">[localPathToXmlPP]</OutArgument>    </Assign.To>    <Assign.Value>      <InArgument x:TypeArguments="x:String" xml:space="preserve">[System.IO.Path.Combine(System.IO.Path.GetDirectoryName(localBuildTemplatePath),"XmlPreprocess.exe")]</InArgument>    </Assign.Value>  </Assign>  <Assign DisplayName="Assign localPathToSpreadsheet">    <Assign.To>      <OutArgument x:TypeArguments="x:String">[localPathToSpreadsheet]</OutArgument>    </Assign.To>    <Assign.Value>      <InArgument x:TypeArguments="x:String" xml:space="preserve">[System.IO.Path.Combine(  System.IO.Path.GetDirectoryName(localBuildTemplatePath),  "SettingsSpreadsheet.xml")]</InArgument>    </Assign.Value>  </Assign>  <mtbwa:DownloadFiles DeletionId="{x:Null}" DisplayName="Download SettingsSpreadsheet.xml" LocalPath="[localPathToSpreadsheet]" ServerPath="[SettingsSpreadsheetPath]" mva:VisualBasic.Settings="Assembly references and imported namespaces serialized as XML namespaces" />  <mtbwa:FindMatchingFiles DisplayName="Find *.config Files" MatchPattern="[String.Format(&quot;{0}\**\*.config&quot;, DropLocation)]" Result="[configFiles]" />  <ParallelForEach x:TypeArguments="x:String" DisplayName="Run XmlPP for Each .config" Values="[configFiles]">    <ActivityAction x:TypeArguments="x:String">      <ActivityAction.Argument>        <DelegateInArgument x:TypeArguments="x:String" Name="configFile" />      </ActivityAction.Argument>      <mtbwa:InvokeProcess Arguments="[String.Format(&quot;/input:{0} /spreadsheet:{1} /environment:{2}&quot;, configFile, localPathToSpreadsheet, TargetEnvironment)]" DisplayName="XmlPreprocess" FileName="[localPathToXmlPP]">        <mtbwa:InvokeProcess.ErrorDataReceived>          <ActivityAction x:TypeArguments="x:String">            <ActivityAction.Argument>              <DelegateInArgument x:TypeArguments="x:String" Name="errOutput" />            </ActivityAction.Argument>          </ActivityAction>        </mtbwa:InvokeProcess.ErrorDataReceived>        <mtbwa:InvokeProcess.OutputDataReceived>          <ActivityAction x:TypeArguments="x:String">            <ActivityAction.Argument>              <DelegateInArgument x:TypeArguments="x:String" Name="stdOutput" />            </ActivityAction.Argument>          </ActivityAction>        </mtbwa:InvokeProcess.OutputDataReceived>      </mtbwa:InvokeProcess>    </ActivityAction>  </ParallelForEach></Sequence>

Listato 1 – XAML della personalizzazione

Per maggior sicurezza si può aggiungere un quinto passo per cancellare il file Excel dall’area di lavoro della build.

Purtroppo non esiste una activity ‘primitiva’ per la cancellazione, ma la soluzione è semplicissima.

GVTFS12_2013-02-07_00008GVTFS12_2013-02-07_00007

Figura 4 – Aggiunta della cancellazione del file Excel

L’ azione è semplicemente:

5. chiamo il metodo File.Delete per il file Excel

Il sorgente è riportato nel Listato 2.

 <InvokeMethod DisplayName="Delete local Spreadsheet" MethodName="Delete" TargetType="si:File">  <InArgument x:TypeArguments="x:String">[localPathToSpreadsheet]</InArgument></InvokeMethod>

Listato 2 – XAML cancellazione finale

Come deve esser fatta la definizione di una build

La definizione per la build non è molto diversa dal quella di default.

GVTFS12_2013-02-07_00004

Figura 5 – Definizione della build

I tre elementi da cambiare sono evidenziati in Figura 1: si sceglie il Build process template personalizzato e si inseriscono i valori desiderati per indicare il file Excel da usare per le sostituzioni e l’ambiente destinatario.

È importantissimo che il workspace comprenda la cartella con Build process template, come indicato in Figura 6.

GVTFS12_2013-01-31_00001

Figura 6 – Workspace che include il Build template

In caso contrario la build termina con un errore simile a questo

1 error(s), 0 warning(s)

imageException Message: There is no working folder mapping for $/EsperimentiBuild/BuildProcessTemplates/XmlPreprocess/XmlPreprocessBuildProcessTemplate.xaml. (type ItemNotMappedException)Exception Stack Trace: at Microsoft.TeamFoundation.VersionControl.Client.Workspace.GetLocalItemForServerItem(String serverItem, Boolean detectImplicitCloak) at Microsoft.TeamFoundation.Build.Workflow.Activities.ConvertWorkspaceItem.Execute(CodeActivityContext context) at System.Activities.CodeActivity`1.InternalExecute(ActivityInstance instance, ActivityExecutor executor, BookmarkManager bookmarkManager) at System.Activities.Runtime.ActivityExecutor.ExecuteActivityWorkItem.ExecuteBody(ActivityExecutor executor, BookmarkManager bookmarkManager, Location resultLocation)

Una build che si conclude correttamente, avrà invece un log simile a quello in Figura 7.

GVTFS12_2013-02-06_00006

Figura 7 – Build Log favorevole

In un log dettagliato potremo vedere una riga simile a questa

X:\Builds\1\EsperimentiBuild\XmlPreprocess_Dev\src\BuildProcessTemplates\XmlPreprocess\XmlPreprocess.exe /input:\\gvtfs12\drops\XmlPreprocess_Dev\XmlPreprocess_Dev_20130131.6\ConsoleApplication1.exe.config /spreadsheet:X:\Builds\1\EsperimentiBuild\XmlPreprocess_Dev\src\BuildProcessTemplates\XmlPreprocess\SettingsSpreadsheet.xml /environment:Integration

Conclusioni

L’esempio può essere immediatamente usato al fine di predisporre i file di configurazione, che si trovano nello share di Drop, per uno specifico ambiente target. Con più build definition preparo i pacchetti per ambienti differenti.

In generale, si son viste delle tecniche utili per integrare il lancio di un tool eseguibile all’interno del processo di build: calcolo di dove si trova l’eseguibile, messa in sicurezza di dati, individuazione di file da elaborare.

Il codice è applicabile sia a Team Foundation Server 2010 che 2012 che a Team Foundation Service.