Continuous Integration: modifichiamo CruiseControl.NET

Come anticipato in uno dei miei recenti post, vorrei parlare di alcune modifiche fatte sui sorgenti di CruiseControl.NET per adattarlo ad una nostra particolare esigenza: la possibilità di determinare la label da attribuire ad una build dopo che i sorgenti di un progetto sono stati scaricati e compilati sulla build-machine.
Non è mia intenzione spiegare l'architettura di CC.NET, servirebbe un libro intero, quindi vado al sodo.

La versione "ufficiale" di CC.NET esige che il contenuto di una label venga costruito all'inizio del processo di integrazione. Se si esamina la classe IntegrationRunner si nota il metodo RunIntegration:

public IIntegrationResult RunIntegration(BuildCondition buildCondition)
{
    IIntegrationResult result = resultManager.StartNewIntegration(buildCondition);
    IIntegrationResult lastResult = resultManager.LastIntegrationResult;

    CreateDirectoryIfItDoesntExist(result.WorkingDirectory);
    CreateDirectoryIfItDoesntExist(result.ArtifactDirectory);
    result.MarkStartTime();
    
try
    
{
        result.Modifications = GetModifications(lastResult, result);
        
if (result.ShouldRunBuild())
        {
            target.Activity = ProjectActivity.Building;
            target.SourceControl.GetSource(result);
            RunBuild(result);
        }
    }
    
catch (Exception ex)
    {
        Log.Error(ex);
        result.ExceptionResult = ex;
    }
    result.MarkEndTime();

    PostBuild(result);

    
return result;
}

Questo metodo è il cuore del sistema: si notino i riferimenti al source control, al processo di build ed al post-build step. Si faccia attenzione anche alla prima linea di codice dove compare la chiamata al metodo StartNewIntegration, che appartiene all'interfaccia IIntegrationResultManager:

public interface IIntegrationResultManager
{
    IIntegrationResult LastIntegrationResult { 
get; }

    IIntegrationResult StartNewIntegration(BuildCondition buildCondition);
    
void FinishIntegration();
}

Tale interfaccia è implementata dalla classe IntegrationResultManager. Il metodo StartNewIntegration si presenta così:

public IIntegrationResult StartNewIntegration(BuildCondition buildCondition)
{
    currentResult = 
new IntegrationResult(project.Name, project.WorkingDirectory);
    currentResult.LastIntegrationStatus = LastIntegrationResult.Status;
    currentResult.BuildCondition = DetermineBuildCondition(buildCondition);
    currentResult.Label = project.Labeller.Generate(LastIntegrationResult);
    currentResult.ArtifactDirectory = project.ArtifactDirectory;
    currentResult.ProjectUrl = project.WebURL;
    currentResult.LastSuccessfulIntegrationLabel =
        LastIntegrationResult.LastSuccessfulIntegrationLabel;
    
return currentResult;
}

La label "nasce" uffialmente grazie alla chiamata al metodo Generate del Labeller.
L'interfaccia ILabeller è molto semplice:

public interface ILabeller : ITask
{
    
string Generate(IIntegrationResult resultFromLastBuild);
}

CC.NET usa un'oggetto di tipo ILabeller per determinare la label. Ne esistono diverse implementazioni e naturalmente è possibile creare le proprie. Ciò che non è possibile fare è decidere il momento in cui il labeller fa il proprio dovere perché l'invocazione del metodo Generate è schiantata in StartNewIntegration.

Perché ci preme tanto decidere quando costruire la label?
Perché vorremmo costruire la label a partire dalle informazioni contenute nei sorgenti, quindi abbiamo bisogno che il download dal VCS preceda la chiamata al labeller.
Per fare questo occorre:
- ovviamente costruire un nostro labeller che si occupi di interpretare i sorgenti (argomento di un prossimo post);
- modificare CC.NET affinchè la creazione della label sia posticipata.

Per questioni di compatibilità abbiamo deciso di non spostare la chiamata del labeller, ma di introdurre un paio di nuove interfacce ed un po' di codice che le gestisca.
Abbiamo creato la nuova interfaccia IPostBuildLabeller per dotare i labeller della possibilità di essere "svegliati" nella fase di post-build:

public interface IPostBuildLabeller : ILabeller
{
    
string PostBuildGenerate(IIntegrationResult resultFromLastBuild);
}

e l'interfaccia IPostBuildIntegrationResultManager per identificare l'inizio della fase di post-build:

public interface IPostBuildIntegrationResultManager : IIntegrationResultManager
{
    IIntegrationResult StartPostBuildStep();
}

A questo punto siamo intervenuti sulla dichirazione della classe IntegrationResultManager dotandola di IPostBuildIntegrationResultManager:

public class IntegrationResultManager
    : IIntegrationResultManager, IPostBuildIntegrationResultManager

e naturalmente implementando il metodo StartPostBuildStep:

public IIntegrationResult StartPostBuildStep()
{
    
try
    
{
        IPostBuildLabeller postBuildLabeller =
            (IPostBuildLabeller)project.Labeller;
        currentResult.Label =
            postBuildLabeller.PostBuildGenerate( LastIntegrationResult );
    }
    
catch ( InvalidCastException ex )
    {
        Log.Info( ex.Message );
    }

    
return currentResult;
}

Questo consente ai vecchi labeller, che non espongono IPostBuildLabeller , di funzionare ugualmente.
Rimane da scrivere il codice che esegua effettivamente la chiamata a quest'ultimo metodo. La classe IntegrationRunner ha un metodo PostBuild che fa al caso nostro. Ecco il codice già modificato:

private void PostBuild(IIntegrationResult result)
{
    
if (ShouldPublishResult(result))
    {
        
// Start: post-build step management
        
try
        
{
            IPostBuildIntegrationResultManager postBuildResultManager =
                (IPostBuildIntegrationResultManager)resultManager;
            result = postBuildResultManager.StartPostBuildStep();
        }
        
catch ( InvalidCastException ex )
        {
            Log.Info( ex.Message );
        }
        
// end: post-build step management

        
LabelSourceControl(result);
        target.PublishResults(result);
        resultManager.FinishIntegration();
    }
    Log.Info("Integration complete: " + result.EndTime);

    target.Activity = ProjectActivity.Sleeping;
}

Questa soluzione ha risolto il nostro problema, con nostra somma felicità; non è la soluzione più flessibile perché in realtà sposta il problema, ma per ora ci basta.
Un'idea potrebbe essere quella di marcare un labeller con un attributo che notifichi a CC.NET quando scatenare la generazione...ovviamente le modifiche sul codice sarebbero più pesanti.
Un'altra idea potrebbe essere quella di basarsi sull'intefaccia ITask, che in CC.NET può essere usata per fare praticamente qualsiasi cosa.

Nota: da quando ho scaricato i sorgenti a quando ho testato la modifica non è passata neppure un'ora. Un genio? Per niente...semplicemente è il codice ad avermi consentito tempi del genere; i tipi ed i metodi hanno nomi parlanti, le implementazioni sono corte, il disaccoppiamento è elevato. Si vede il lavoro di un certo Martin Fowler


powered by IMHO 1.3

posted @ lunedì 4 settembre 2006 01:41

Print