Java 项目中系统构建编号和版本号管理的当前最佳实践是什么? 具体来说:
如何在分布式开发环境中系统地管理构建编号
如何在运行时应用程序的源代码/可用版本号中维护版本号
如何正确地与源代码库集成
如何更自动地管理版本号与存储库标记
如何与持续构建基础设施集成
有相当多的工具可用,而 ant (我们正在使用的构建系统)有一个维护构建编号的任务,但是不清楚如何使用 CVS、 svn 或类似的多个并发开发人员来管理这个任务。
[编辑]
下面出现了一些好的、有帮助的部分或具体的答案,所以我将总结其中的一些。在我看来,在这个问题上并没有一个强有力的“最佳实践”,而是一系列重叠的想法。下面是我的总结和一些结果问题,大家可能会尝试作为后续回答。[ stackoverflow 新手... ... 如果我做错了,请提供评论。]
如果您正在使用 SVN,那么特定签出的版本控制将随之而来。构建编号可以利用这一点来创建一个唯一的构建编号,用于标识特定的签出/修订。[我们使用 CVS 是出于传统的原因,它没有提供这种水平的洞察力... ... 使用标签进行手动干预可以让您在一定程度上达到目的。]
如果您使用 maven 作为构建系统,则支持从 SCM 生成版本号,以及自动生成版本的发布模块。[由于各种原因,我们不能使用 maven,但这对那些能够使用它的人有所帮助。[感谢 Marcelo-Morales]
如果使用 蚂蚁作为构建系统,以下任务描述可以帮助生成 Java。属性文件捕获生成信息,然后可以通过多种方式将这些信息折叠到生成中。[感谢 Marty-Lamb,我们将这个想法扩展到包括来自哈德逊的信息]。
Ant 和 maven (以及 Hudson 和巡航控制)提供了将构建数字放入。属性文件,或转换为。Txt/.Html 文件。这种“安全”是否足以防止它被有意或无意地篡改?在构建时将其编译成“版本控制”类是否更好?
断言: 构建编号应该在像 Hudson这样的持续集成系统中定义/实施。[感谢 Marcelo-Morales]我们接受了这个建议,但它确实打开了发布工程学的问题: 一个发布是如何发生的?在一个版本中是否有多个构建编号?不同版本的构建编号之间是否存在有意义的关系?
问: 构建编号背后的目标是什么?是否用于质量保证?怎么做到的?它主要被开发人员用来在开发过程中消除多个构建之间的歧义,还是更多地被 QA 用来确定最终用户得到了什么构建?如果目标是可重复性,那么在理论上,这就是版本号应该提供的——为什么没有呢?(请在下面的回答中回答这个问题,这将有助于阐明你所做的选择或建议... ...)
问: 在手动构建中是否有编号的位置?这个问题是否如此严重,以至于每个人都应该使用 CI 解决方案?
问: 构建编号是否应该签入到 SCM?如果目标是可靠且明确地识别特定的构建,那么如何处理各种可能会崩溃/重启的连续或手动构建系统。.
问题: 一个构建编号是否应该简短(例如,单调递增的整数) ,以便容易粘贴到文件名中以便存档,容易在通信中引用,等等。.或者它应该是长和充分的用户名,日期戳,机器名称等?
问题: 请提供关于如何将构建编号分配到更大的自动化发布过程中的详细信息。是的,玛文爱好者们,我们知道这已经是板上钉钉的事了,但是并不是我们所有人都已经喝下了酷爱饮料..。
我真的很想把这个问题充实成一个完整的答案,至少对于我们的 cvs/ant/Hudson 设置的具体例子来说是这样的,这样就有人可以根据这个问题构建一个完整的策略。对于这种特殊情况,任何能够给出详尽描述的人(包括 cvs 标记方案、相关的 CI 配置项,以及将构建号折叠到版本中以便通过编程可访问的发布过程) ,我都会标记为“答案”如果您想询问/回答另一个特定的配置(比如 svn/maven/巡航控制) ,我将从这里链接到这个问题。—— JA
[2009年10月23日编辑] 我接受了最受欢迎的答案,因为我认为这是一个合理的解决方案,而其他几个答案也包括好的想法。如果有人想尝试用 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();
}
}
如果这值得成为一个维基讨论,请留言。