什么是Java项目中的系统构建编号和版本号管理的当前最佳实践?特别:
>如何在分布式开发环境中系统地管理构建号
>如何维护源代码中的版本号/可用于运行时应用程序
>如何正确地与源存储库集成
>如何更自动地管理版本号与存储库标签
>如何与持续构建基础架构集成
有相当多的工具可用,和ant(我们使用的构建系统)有一个任务将维护一个内部版本号,但不清楚如何管理这个与多个并发开发人员使用CVS,svn或类似。
[编辑]
下面出现几个好的和有帮助的部分或具体的答案,所以我将总结其中几个。这听起来像我一样,没有真正的强大的“最佳实践”,而是一系列重叠的想法。下面,找到我的摘要和一些结果问题,人们可能会试图回答作为后续。 [新的stackoverflow …请提供意见,如果我做错了。]
>如果您使用SVN,特定签出的版本控制将随之而来。构建编号可以利用此来创建唯一的构建号,以标识特定的检出/修订。 [CVS,我们使用的是遗留的原因,不提供相当这样的洞察…手动干预标签得到你的一部分。
>如果您使用maven作为构建系统,则支持从SCM生成版本号,以及用于自动生成版本的发布模块。 [我们不能使用maven,出于各种原因,但这有助于那些可以。 [感谢marcelo-morales]]
>如果您使用ant作为构建系统,以下任务描述可以帮助生成捕获构建信息的Java .properties文件,然后可以通过多种方式将其折叠到构建中。 [我们扩展了这个想法,包括哈德森派生的信息,感谢marty-lamb]。
> Ant和maven(以及哈德逊和巡航控制)提供了将构建号存入.properties文件或.txt / .html文件的简单方法。这是“安全”,足以防止它被有意或无意地篡改吗?是否最好在构建时将其编译为“版本化”类?
>断言:构建编号应该在像hudson这样的连续集成系统中定义/制定[感谢marcelo-morales]我们采纳了这个建议,但它确实打开了发布工程问题:发布是如何发生的?发布中有多个buildnumbers吗?不同版本的构建器之间是否有有意义的关系?
>问题:构建号码背后的目标是什么?是用于QA吗?怎么样?它是主要由开发人员在开发过程中消除多个构建之间的歧异,或更多的QA来确定最终用户构建什么?如果目标是可重复性,理论上这是发布版本号应该提供什么 – 为什么不? (请回答这个作为您的答案的一部分,在下面,它将有助于照亮你已经/建议的选择…)
>问题:在手动构建中是否有构建号的地方?这是否有什么问题,每个人都应该使用CI解决方案?
>问题:应该将内部编号签入SCM吗?如果目标是可靠和明确地识别特定构建,如何处理可能崩溃/重启等各种连续或手动构建系统…
>问题:如果一个构建号是短和甜的(即单调增加整数),以便它容易粘到文件名中,以便归档,容易参考在通信等…或应该是长而充满的用户名,日期戳,机器名等?
>问题:请提供详细信息,了解内部版本号的分配如何适用于更大的自动发行流程。是的,maven爱好者,我们知道这是做了,做了,但不是我们所有的人都喝了kool助剂还… …
我真的想把这个东西变成一个完整的答案,至少对于我们的cvs / ant / hudson设置的具体例子,所以有人可以基于这个问题构建一个完整的策略。我将标记为“答案”任何人谁可以给这个特定的情况下汤 – 坚果描述(包括cvs标记方案,相关的配置项目和发布过程,将版本号折叠到版本中,以编程方式如果你想问/回答另一个特定的配置(比如,svn / maven /巡航控制),我将链接到这里的问题。 –JA
[EDIT 23 Oct 09]
我接受了最高票数的答案,因为我认为这是一个合理的解决方案,而其他几个答案也包括好的想法。如果有人想用marty-lamb的合成一些裂纹,我会考虑接受一个不同的。我对marty-lamb唯一关心的是它不产生可靠的序列化构建号 – 它依赖于构建器系统上的本地时钟来提供明确的构建号,这不是很好。
[编辑7月10]
我们现在包括一个类如下。这允许将版本号编译成最终的可执行文件。版本信息的不同形式在日志数据(长期归档输出产品)中发出,用于跟踪我们(有时是多年后)对特定版本的输出产品的分析。
public final class AppVersion
{
// SVN should fill this out with the latest tag when it's checked out.
private static final String APP_SVNURL_RAW =
"$HeadURL: svn+ssh://user@host/svnroot/app/trunk/src/AppVersion.java $";
private static final String APP_SVN_REVISION_RAW = "$Revision: 325 $";
private static final Pattern SVNBRANCH_PAT =
Pattern.compile("(branches|trunk|releases)\\/([\\w\\.\\-]+)\\/.*");
private static final String APP_SVNTAIL =
APP_SVNURL_RAW.replaceFirst(".*\\/svnroot\\/app\\/", "");
private static final String APP_BRANCHTAG;
private static final String APP_BRANCHTAG_NAME;
private static final String APP_SVNREVISION =
APP_SVN_REVISION_RAW.replaceAll("\\$Revision:\\s*","").replaceAll("\\s*\\$", "");
static {
Matcher m = SVNBRANCH_PAT.matcher(APP_SVNTAIL);
if (!m.matches()) {
APP_BRANCHTAG = "[Broken SVN Info]";
APP_BRANCHTAG_NAME = "[Broken SVN Info]";
} else {
APP_BRANCHTAG = m.group(1);
if (APP_BRANCHTAG.equals("trunk")) {
// this isn't necessary in this SO example, but it
// is since we don't call it trunk in the real case
APP_BRANCHTAG_NAME = "trunk";
} else {
APP_BRANCHTAG_NAME = m.group(2);
}
}
}
public static String tagOrBranchName()
{ return APP_BRANCHTAG_NAME; }
/** Answers a formatter String descriptor for the app version.
* @return version string */
public static String longStringVersion()
{ return "app "+tagOrBranchName()+" ("+
tagOrBranchName()+", svn revision="+svnRevision()+")"; }
public static String shortStringVersion()
{ return tagOrBranchName(); }
public static String svnVersion()
{ return APP_SVNURL_RAW; }
public static String svnRevision()
{ return APP_SVNREVISION; }
public static String svnBranchId()
{ return APP_BRANCHTAG + "/" + APP_BRANCHTAG_NAME; }
public static final String banner()
{
StringBuilder sb = new StringBuilder();
sb.append("\n----------------------------------------------------------------");
sb.append("\nApplication -- ");
sb.append(longStringVersion());
sb.append("\n----------------------------------------------------------------\n");
return sb.toString();
}
}
如果这应该成为一个wiki讨论,请发表评论。