Monday, April 4, 2011

Tuning CCNet to your whishes : a new trigger

There was a request on the user list to run a nightly "publish" build, which takes the output of the other "child" projects and copies them to a new output directory. Since the output is quite large (> 1 GB), this should only be done each night if something changed in any of the other projects, but only if all of them have run successfully.
You can read the full details also on Stack overflow. Now one should be able to do this using the existing functionality, but that is not the case :-(. The reason is mainly that CCNet has grown from a simple CI server to a more feature rich one over the years, but here and there are still leftovers from the beginning, read the simple CI server. Triggers are just events that occur, and do not know a state, there is the project trigger, but that is more of a fire and forget one. So restarting a server poses a problem, combining multiple project triggers into one trigger poses a problem with timings over a day and so on.

To handle this situation, the only option is to write some code that does it. Thankfully CCNet has a plugin architecture that works quite well, see my previous creating a plugin about it for more details.

As an example of this request, I will create a new trigger, because this will be the fastest way to get a result with just a few lines of code adjustments needed. For starters I just copied the code of the existing schedule trigger into a new class, named ScheduleTriggerExtended.

Changes done :
° added property SubProjectsToWatch
The project names of the child projects to watch. For simplicity only the project name, meaning that it has to be on the same build server and has browse rights (or no security setup)
° added property ServerUri
the uri of the ccnet server, example : tcp://localhost:21234/CruiseServerClient.rem
° added property SubProjectsSavePath
A path reachable by the CCNet server (preferably local). This is used to store the timestamps of the subprojects. So we can check if the timestamp changed between the integration of a subproject and the build of the involved subproject.
° updated public IntegrationRequest Fire
Whenever the scheduled time is reached, also check if the subProjects are changed, by comparing the current last build times with the saved ones.
° updated public void IntegrationCompleted
Save the new buildtimes of the subprojects to the SubProjectsSavePath for later reference.

When reading this all over, the best approach would be to create a 'CCNet source control', that can scan other CCNet projects for changes, and filter them according to some filters (state, build time, ...)

Anyway, below is the trigger class, all changes are highlighted with comment : added code. Copy entire source into an assembly with name something like : CCNet.whatever.plugin
Example : CCNet.RuWi.plugin
And set references to NetReflector.dll, ThoughtWorks.CruiseControl.Core.dll, ThoughtWorks.CruiseControl.Remote.dll


A config is also provided below the code.

using System;
using System.Globalization;
using Exortech.NetReflector;
using ThoughtWorks.CruiseControl.Core.Config;
using ThoughtWorks.CruiseControl.Core.Util;
using ThoughtWorks.CruiseControl.Remote;


namespace ThoughtWorks.CruiseControl.Core.Triggers
{

[ReflectorType("scheduleTriggerExtended")]
public class ScheduleTriggerExtended : ITrigger, IConfigurationValidation
{
private string name;
private DateTimeProvider dtProvider;
private TimeSpan integrationTime;
private DateTime nextBuild;
private bool triggered;
private Int32 randomOffSetInMinutesFromTime/* = 0*/;
Random randomizer = new Random();

/// <summary>
/// Initializes a new instance of the <see cref="ScheduleTrigger"/> class.
/// </summary>
public ScheduleTriggerExtended()
: this(new DateTimeProvider())
{
}

/// <summary>
/// Initializes a new instance of the <see cref="ScheduleTrigger"/> class.
/// </summary>
/// <param name="dtProvider">The dt provider.</param>
public ScheduleTriggerExtended(DateTimeProvider dtProvider)
{
this.dtProvider = dtProvider;
this.BuildCondition = BuildCondition.IfModificationExists;
WeekDays = (DayOfWeek[])DayOfWeek.GetValues(typeof(DayOfWeek));
}

/// <summary>
/// The time of day that the build should run at. The time should be specified in a locale-specific format (ie. H:mm am/pm is acceptable for US locales.)
/// </summary>
/// <version>1.0</version>
/// <default>n/a</default>
[ReflectorProperty("time")]
public virtual string Time
{
get { return integrationTime.ToString(); }
set
{
try
{
integrationTime = TimeSpan.Parse(value);
}
catch (Exception ex)
{
string msg = "Unable to parse daily schedule integration time: {0}. The integration time should be specified in the format: {1}.";
throw new ConfigurationException(string.Format(CultureInfo.CurrentCulture, msg, value, CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern), ex);
}
}
}

/// <summary>
/// Adds a random amount of minutes between 0 and set value to the time. This is mainly meant for spreading the load of actions to a central server.
/// Value must be between 0 and 59.
/// </summary>
/// <version>1.4</version>
/// <default>0</default>
[ReflectorProperty("randomOffSetInMinutesFromTime", Required = false)]
public int RandomOffSetInMinutesFromTime
{
get { return randomOffSetInMinutesFromTime; }
set
{
randomOffSetInMinutesFromTime = value;
if (randomOffSetInMinutesFromTime < 0 || randomOffSetInMinutesFromTime >= 60)
throw new ConfigurationException("randomOffSetInMinutesFromTime must be in the range 0 - 59");
}
}

/// <summary>
/// The name of the trigger. This name is passed to external tools as a means to identify the trigger that requested the build.
/// </summary>
/// <version>1.1</version>
/// <default>ScheduleTrigger</default>
[ReflectorProperty("name", Required = false)]
public string Name
{
get
{
if (name == null) name = GetType().Name;
return name;
}
set { name = value; }
}

/// <summary>
/// The condition that should be used to launch the integration. By default, this value is <b>IfModificationExists</b>, meaning that an integration will
/// only be triggered if modifications have been detected. Set this attribute to <b>ForceBuild</b> in order to ensure that a build should be launched
/// regardless of whether new modifications are detected.
/// </summary>
/// <version>1.0</version>
/// <default>IfModificationExists</default>
[ReflectorProperty("buildCondition", Required = false)]
public BuildCondition BuildCondition { get; set; }

/// <summary>
/// The week days on which the build should be run (eg. Monday, Tuesday). By default, all days of the week are set.
/// </summary>
/// <version>1.0</version>
/// <default>Monday-Sunday</default>
[ReflectorProperty("weekDays", Required = false)]
public DayOfWeek[] WeekDays { get; set; }


//added code

/// <summary>
/// The project names of the child projects to watch. For cimplicity only the project name,
/// meaning that it has to be on the same build server and has browse rights (or no security setup)
/// </summary>
[ReflectorArray("subProjectsToWatch", Required = true)]
public string[] SubProjectsToWatch { get; set; }


/// <summary>
/// the uri of the ccnet server, example : tcp://localhost:21234/CruiseServerClient.rem
/// </summary>
[ReflectorProperty("serverUri", Required = true)]
public string ServerUri { get; set; }



/// <summary>
/// A path reachable by the CCNet server (preferably local). This is used to store the timestamps of the subprojects.
/// So we can check if the timestamp changed between the integration of a subproject and the build of the involved subproject.
/// </summary>
[ReflectorProperty("subProjectsSavePath", Required = true)]
public string SubProjectsSavePath { get; set; }


/// <summary>
/// Looks at the timestamp saved in the reference file of the subprojects. If that timestamp differs from the last
/// integration timestamp of the subproject, a re-build is needed.
/// </summary>
/// <returns></returns>
private bool SubProjectsAreChanged()
{
// nothing to watch
if (SubProjectsToWatch == null || SubProjectsToWatch.Length == 0) return false;

//the save path does not exist, so we must do the first build
if (!System.IO.Directory.Exists(SubProjectsSavePath)) return true;

//get the last build info from the build server
var cm = new RemoteCruiseManagerFactory().GetCruiseManager(ServerUri);

var buildInfos = cm.GetCruiseServerSnapshot().ProjectStatuses;


foreach (string subprojectName in SubProjectsToWatch)
{
string locationOnDisk = System.IO.Path.Combine(SubProjectsSavePath, subprojectName);

if (! System.IO.File.Exists(locationOnDisk)) return true;

DateTime LastTimeBuildInIntegration;

string data = System.IO.File.ReadAllText(locationOnDisk);

if (DateTime.TryParse(data, out LastTimeBuildInIntegration))
{
var lastbuildinfo = GetSubProjectLastBuildInfo(subprojectName, buildInfos);

if (lastbuildinfo.BuildStatus != IntegrationStatus.Success) return false;

if (LastTimeBuildInIntegration < lastbuildinfo.LastBuildDate ) return true;
}
else
{
// file is empty, the contents is not a date, is corrupted, ....
throw new Exception(string.Format("Contents of {0} --{1}-- is not convertable to a datetime", locationOnDisk,data));
}
}

return false;
}

private Remote.ProjectStatus GetSubProjectLastBuildInfo(string projectName, Remote.ProjectStatus[] serverInfo)
{
foreach (ProjectStatus ps in serverInfo)
{
if (ps.Name == projectName) return ps;
}

throw new Exception(string.Format("Project {0} not found on server with uri {1}", projectName, ServerUri));
}


private void SaveSubProjectsLastBuildTimes()
{
// nothing to watch
if (SubProjectsToWatch == null || SubProjectsToWatch.Length == 0) return ;

if (!System.IO.Directory.Exists(SubProjectsSavePath)) System.IO.Directory.CreateDirectory(SubProjectsSavePath);

DateTime now = dtProvider.Now;

foreach (string subprojectName in SubProjectsToWatch)
{
string locationOnDisk = System.IO.Path.Combine(SubProjectsSavePath, subprojectName);

System.IO.File.WriteAllText(locationOnDisk, now.ToLongDateString());
}
}

//end added code


private void SetNextIntegrationDateTime()
{

if (integrationTime.Minutes + RandomOffSetInMinutesFromTime >= 60)
throw new ConfigurationException(string.Format(System.Globalization.CultureInfo.CurrentCulture, "Scheduled time {0}:{1} + randomOffSetInMinutesFromTime {2} would exceed the hour, this is not allowed", integrationTime.Hours, integrationTime.Minutes, RandomOffSetInMinutesFromTime));

DateTime now = dtProvider.Now;
nextBuild = new DateTime(now.Year, now.Month, now.Day, integrationTime.Hours, integrationTime.Minutes, 0, 0);

if (randomOffSetInMinutesFromTime > 0)
{
Int32 randomNumber = randomizer.Next(randomOffSetInMinutesFromTime);
nextBuild = nextBuild.AddMinutes(randomNumber);
}

if (now >= nextBuild)
{
nextBuild = nextBuild.AddDays(1);
}

nextBuild = CalculateNextIntegrationTime(nextBuild);
}

private DateTime CalculateNextIntegrationTime(DateTime nextIntegration)
{
while (true)
{
if (IsValidWeekDay(nextIntegration.DayOfWeek))
break;
nextIntegration = nextIntegration.AddDays(1);
}
return nextIntegration;
}

private bool IsValidWeekDay(DayOfWeek nextIntegrationDay)
{
return Array.IndexOf(WeekDays, nextIntegrationDay) >= 0;
}

/// <summary>
/// Integrations the completed.
/// </summary>
/// <remarks></remarks>
public virtual void IntegrationCompleted()
{
if (triggered)
{
// added code
SaveSubProjectsLastBuildTimes();
// added code
SetNextIntegrationDateTime();
}
triggered = false;
}

/// <summary>
/// Gets the next build.
/// </summary>
/// <value></value>
/// <remarks></remarks>
public DateTime NextBuild
{
get
{
if (nextBuild == DateTime.MinValue)
{
SetNextIntegrationDateTime();
}
return nextBuild;
}
}

/// <summary>
/// Fires this instance.
/// </summary>
/// <returns></returns>
/// <remarks></remarks>
public IntegrationRequest Fire()
{
DateTime now = dtProvider.Now;

//added code
//if (now > NextBuild && IsValidWeekDay(now.DayOfWeek))
if (now > NextBuild && IsValidWeekDay(now.DayOfWeek) && SubProjectsAreChanged())
// end added code
{
triggered = true;

return new IntegrationRequest(BuildCondition, Name, null);
}
return null;
}


void IConfigurationValidation.Validate(IConfiguration configuration, ConfigurationTrace parent, IConfigurationErrorProcesser errorProcesser)
{
string projectName = "(Unknown)";

var project = parent.GetAncestorValue<Project>();
if (project != null)
{
projectName = project.Name;
}

if (integrationTime.Minutes + RandomOffSetInMinutesFromTime >= 60)
{
errorProcesser.ProcessError("Scheduled time {0}:{1} + randomOffSetInMinutesFromTime {2} would exceed the hour, this is not allowed. Conflicting project {3}", integrationTime.Hours, integrationTime.Minutes, RandomOffSetInMinutesFromTime, projectName);
}
}

}
}



Example Config

<cruisecontrol xmlns:cb="urn:ccnet.config.builder">

<project name="Child1" >
<artifactDirectory>D:\temp\Integration\Child1</artifactDirectory>
<workingDirectory>D:\temp\Integration\Child1</workingDirectory>

<triggers />
<tasks >
<nullTask/>
</tasks>

<publishers>
<xmllogger />
</publishers>

</project>

<project name="Child2" >
<artifactDirectory>D:\temp\Integration\Child2</artifactDirectory>
<workingDirectory>D:\temp\Integration\Child2</workingDirectory>

<triggers />
<tasks >
<nullTask/>
</tasks>

<publishers>
<xmllogger />
</publishers>

</project>

<project name="Child3" >
<artifactDirectory>D:\temp\Integration\Child3</artifactDirectory>
<workingDirectory>D:\temp\Integration\Child3</workingDirectory>

<triggers />
<tasks >
<nullTask/>
</tasks>

<publishers>
<xmllogger />
</publishers>

</project>


<project name="Integrator" >
<artifactDirectory>D:\temp\Integration\Intgrator</artifactDirectory>
<workingDirectory>D:\temp\Integration\Integrator</workingDirectory>

<triggers>
<scheduleTriggerExtended
time="14:08"
buildCondition="IfModificationExists"
name="Scheduled"
serverUri="tcp://localhost:21234/CruiseServerClient.rem"
subProjectsSavePath="d:\temp">

<subProjectsToWatch>
<name>Child1</name>
<name>Child3</name>
</subProjectsToWatch>

<weekDays>
<weekDay>Saturday</weekDay>
</weekDays>

</scheduleTriggerExtended>
</triggers>

<tasks >
<nullTask/>
</tasks>

<publishers>
<xmllogger />
</publishers>

</project>

</cruisecontrol>



Monday, February 21, 2011

Cradiator : WPF frontend for CCNet

I've been working on improving my WPF skills, and what's better than making a WPF front-end for CCNet? Good thing is that this already exists : Cradiator.

I've made a fork on github, so that PandaWood (the maintainer of Cradiator) can pull in my changes as he wants. Since this is my first encounter with Git, I must admit that it takes some getting used to, but it works like a charm.

You can find my fork here.

I've made the following updates :
° added a server-regex, to filter on (build)server name
° added a name on the view
° shows the amount of projects fetched from the URL
° option 'Show only broken'
° option 'Show server name'
° option 'Show out of date' projects


A bit of explanation on the new options :

Show Only Broken

This shows only the broken projects according to selection (project name, category name, server name). If no broken projects are found a smiley and the view name are shown, so you know which view is ok.

Show Server Name

This just shows the server name below the projectname. Comes in handy when you have projects with the same name on different build servers. (Compile on Windows and on Linux for example)

Show Out Of Date

This shows the projects that are outside of an allowed time difference in minutes. The highest LatestBuildTime of all projects is taken. This comes in handy when you have projects(mostly on different build servers) that should do an action at a certain time. This option allows to show the projects which did not build, regardless if they are broken or not. For example the windows build machine was down from 20:00 to 23:00 hours, but we had a FullBuild defined at 22:00.
Now Cradiator shows that the FullBuild on the windows server did not take place.

I use this for some time at work, and it is great. This makes it a lot easier to keep an eye on all the installations I do with CCNet. Remember 80+ servers with each about 30 projects, this makes 2400 projects to watch, so some filtering is needed.

If you get the code and build it, you can see all the new options in action, I update the debugProjects file to show them without needing to point your buildserver.

Suggestions or remarks are welcome

Sunday, January 9, 2011

WPF : a new strategy

So I gave up WPF a year ago, and went back to the basic business programming :-) Even at work I do not do any WPF anymore.
This was very successful, but now I got a problem : business programming is all done and fast, but the WPF side still needs work. Having a bad feeling, I started learning WPF, again.. .

But for some reason I stumbled on a great resource, I did not find a year ago : WPF MVVM Video by Jason Dolinger. Now I find this a must see video for anyone starting WPF or MVVM! It is 1 hour, but it tackles almost everything for WPF - MVVM programming. No fancy UI stuff, but data handling! It is a simple application, but Jason takes it through an entire re-factoring cycle :
° starting the win-form way (like most people tend to do when first doing WPF)
° introducing a basic view-model
° a view-model with commands
° adding tests to the view-model
° introducing unity
° ...

This resource really helped me a lot more than the other stuff I found in the past. And now I can search for more specific items if it needs be, since I now know how WPF data handling works. Jason just uses the basic WPF stuff, not 3rd party libs like prism or so, giving you an idea of to start from scratch and what benefits certain frameworks can provide.

Since we're a VB.Net shop, I converted the example to VB.Net what also took a lot of time and effort. There are items in C# that do not have a VB.Net counterpart, or are named differently, for example the yield operator. I only converted the WPF application to VB.Net, and not the dataprovider project, as this is not touched in the video at all. Every big part in the video has its own subfolder and solution, so you can easily see what is changed in what part. I hope you find this conversion useful.

Another reason I looked again at WPF is CCNet vNext, I'm playing with the idea to make the new cctray in wpf, this will give it a new, modern look. And with a bit of luck, we can make the dashbord in silverlight, so we should be able to use the same view-models for cctray and the dashboard!

The source of Jasons video and the VB.net conversion.

Sunday, December 26, 2010

Continous Installation : going beyond the I of installation

The installation of software towards our customers goes very smooth :-) I've set this system up about 2 years ago, and it is a very appreciated part in our company. Hundreds of installations done.
From the start on I extended the functionality beyond the basic installation part, from day 1 actually, but I never wrote about it yet. I also collected some data of those servers : firstly just the application event log. The idea was to send us the error logs, so we were informed if a customer called. And we can do statistics on these errors. What application throws the most errors, are these technical, validation, communication, ... related and so on. Very usefull information.
Later on, I extended it even more to watch the server itself, CPU usage, hard disk usage and even SQL Server information : backup ok, size of database, size of tables, ...
All this info is send to our central SQL server for analysis in the same way as the rest : via the FTP site. This also proved very usefull since we discovered hard disk failures and bad memory modules before they could do serious damage !

But now a new challenge appeared : Hyper-V

Our next servers are more powerfull and we want to reduce the amount of time we need when we have to replace one. The answer is Hyper-V.
These servers are loaded with Hyper-V Core, and above that we will host 1 or more virtual servers. If the hardware needs updating, we can just put in a bigger server with Hyper-V Core on it, move the virtual servers on to it, and we're done. All settings are kept as the were before the move : Big Advantage !!

The catch : we also need to watch the Hyper-V machine itself as we do the current servers. System team looked around for Hyper-V remote stuff, but could not find anything (payable). When I heard of this, I said :
Just use CCNet :-D We use it on all of our other servers and it works, so why not on a Hyper-V one?

And it worked indeed, Hyper-V Core comes with .Net framework 2.O loaded, and that's all I need. To be on the safe side I use the same (very old) version of CCNet as we do on the other servers : 1.4.2 (yikes indeed very old).
But we only use the exec task and the basic triggers, so it is all we need.
just copy the CCNet setup exe on it via a share, run it through a command prompt, and CCNet is installed. Could not be easier !
Now we can monitor and maintain the Hyper-V servers the same as the other ones.

Thursday, August 5, 2010

Continous Installation : Forcing multiple builds at once

It's been about 2 years since I've set up CCNet for Continuous Installation at work, and it works like a charm. Programs we install with CCNet are at time of writing :Off course some programs also require a database, so the database updating is also done as a part of the installation :-).

But now another need has surfaced : Program Dependencies.
We make software for the local government (City hall level), but this is all kinds of software : finance, registry of births, deaths and marriages, construction requests and so on. What we do NOT want is that multiple programs need to handle the state of a person (married, divorced, ...). There is 1 program that handles this kind of stuff, and others can interface with it. For the moment these interfaces are done via WCF services. So there is no 'real' reference like with a function library, but more like with an interface library. And so far this works great.

For example when a new version of lets say Bookkeeping is needed, I may also need to install or update PeopleManagement, because of the new dependency between Bookkeeping and PeopleManagement. Now as long as it is only 1 dependency, I could just schedule them after each other, but there are programs with 5 or more dependencies :-(
And doing it by hand also means that I (or a collegue) may forget to update a dependency, which is not good.

There is the ForceBuild Publisher, but this has a big problem, it just launches the projects with the Fire and Forget strategy. Meaning that I do not have control of the exact order of execution, I have no control what to do when a dependency fails, ...

Luckily the code of that publisher is a good starting point, the only things I need to add is to wait for the outcome, before returning control to CCNet-core and foresee a property to fail the task if the forced build failed.

The current code of the ForceBuild is as follows :
public void Run(IIntegrationResult result)
{
if (IntegrationStatus != result.Status) return;
factory.GetCruiseManager(ServerUri).ForceBuild(Project, BuildForcerName);
}
With the adjustments, the code is :
public void Run(IIntegrationResult result)
{
//get cruisemanager
var cm = factory.GetCruiseManager(ServerUri);
var cpi = GetCurrentProjectInfo(cm);

//get last known build date of project to be forced
var lastBuildDate = cpi.LastBuildDate;
var compareBuildDate = lastBuildDate;

//force build of wanted project
cm.ForceBuild(ProjectName, buildForcerName);

//keep waiting until build is done
while (lastBuildDate.Equals(compareBuildDate))
{
System.Threading.Thread.Sleep(500);
cpi = GetCurrentProjectInfo(cm);
compareBuildDate = cpi.LastBuildDate;
}

if (FailIfForceBuildFaled && cpi.BuildStatus != IntegrationStatus.Success)
{
throw new CruiseControlException("Force Build Failed of : " + ProjectName);
}
}

private ProjectStatus GetCurrentProjectInfo(ICruiseManager cm)
{
var previousBuildInfos = cm.GetCruiseServerSnapshot();
ProjectStatus result = null;

foreach (ProjectStatus ps in previousBuildInfos.ProjectStatuses)
{
if (ps.Name == ProjectName)
{
result = ps;
continue;
}
}

return result;
}


Not much change in code, but a very great increase in the ease of installing our software. Now I just schedule this project, iso many others :-)

Monday, July 19, 2010

back in action

It's been a very long time since I made a post, due to various reasons. My work got all my time due to a project that went bad :-( This is now solved, so meaning more time for CCNet now : Yeah !
I do need to catch up, so give me some time. There is a second release planned of the 1.5 version which should fix some annoying bugs. ETA should be in 1 to 2 weeks. As for the 1.6 release, this will probably be around October or so, depending on the amount of new items.

I did make a small new addition to the 1.6 code base: a Cron Trigger.
For the people working with a Unix system, this sounds familiar, for those not knowing, it is a time-based job scheduler. Cron enables users to schedule jobs (commands or shell scripts) to run periodically at certain times or dates. It is commonly used to automate system maintenance or administration.

Because the Cron syntax is very simple it is also very powerfull. Scheduling something every tuesday every 10 minutes between 15 and 16 hours, but only in the weekends is very easy.
You can see the documentation at Cron Trigger
All the hard work was already done, there is a project NCrontab so it was just adding the code, very easy.

Next items will be crawling through Jira, and fixing open issues, so that will keep me busy for some time.

Tuesday, April 13, 2010

Continuous Installation and MS SQL Reporting Services

A few weeks ago, I was informed that we also will use MS SQL Reporting Services in our programs for the reporting. Now I was used to Crystal Reports, which are easy to deploy because they are compiled into the assembly. But fust for making things more complicated, the company decided to use MS SQL Reporting Services.
From deployment point of view, this is a pain !
The reason : there is no real deployment strategy for seen by MS (as far as I could find out). There are 3 options :
° distribute via the publish in Visual Studio
° use the SOAP interface exposed by MS SQL Reporting Services
° there is an rs.exe, but the usage is just medieval.

Option 1
NO GO, not for 75+ servers !
this requires direct access to all of the servers, can not be sheduled, would be difficult with the current version setup we have, ....

Option 2
This would mean writing a program for communicating with the soap interface, plus its errorhandling, and more important : the data (reports and the like) must still be supplied somehow

Option 3
This seemed the best way to start, and it turned out to be so. The rs.exe is indeed medieval, but there is a nice UI : RSScripter.
RSSCripter makes a batch file that calls rs.exe with all the needed arguments (neat), and exports all wanted items into a folder structure.


So I went for option 3, the approach is as follows :
1) our reporting guy creates the various reports (yes we have a real reporting guru)
2) when he thinks if all are some reports are ok, he uses the RSScripter to export all needed items (reports, datasources, folders, ...)
3) he places this in a dedicated folder in source control, each 'version' is a subfolder of this dedicated folder.
4) he presses 'force build' on the 'make package' project, which just zips the folder

and now I have a package I can deploy :-)

For the deployment itself, there are still some small steps needed though :
° adjust the batch file
a) update the log file location
b) update the scriptlocation
c) update the target reportserver
° adjust some rss files (report and datasource files)
a) update any connection strings
b) update user and password
c) update the source location


when all this is done, I just call the updated batch file.

Nice and easy