本文档面向希望将他们的工作发布为其他程序可以依赖的库的开发人员。本文档逐步介绍在发布开源库之前应该回答的主要问题,并展示典型的开发环境是什么样的。
可以在 https://github.com/scalacenter/library-example 找到遵循此处给出的建议的库示例。为了简洁起见,此示例使用了诸如 GitHub、Travis CI 和 sbt 等常用技术,但会提到其他技术,并且针对它们调整本文档的内容应该很简单。
选择开源许可证
第一步是选择一个开源许可证,指定其他人可以重新使用该库的条件。您可以在 opensource.org 网站上浏览现有的开源许可证。如果您不知道选择哪一个,我们建议使用 Apache License 2.0,它允许用户使用(包括商业用途)、共享、修改和重新分发(包括在不同条款下)您的作品,条件是保留许可证和版权声明。有记录表明,Scala 本身是根据 Apache 2.0 许可的。
选择许可证后,通过在项目的根目录中创建一个 LICENSE
文件,其中包含许可证内容或指向许可证的链接,将许可证应用到您的项目。此文件通常会指明谁拥有版权。在我们的 LICENSE 文件 示例中,我们写道所有贡献者(根据 Git 日志)拥有版权。
托管源代码
我们建议通过在 Git 托管网站(例如 GitHub、Bitbucket 或 GitLab)上托管源代码来共享您的库的源代码。在我们的示例中,我们使用 GitHub。
您的项目应包含一个 README 文件,其中包括库的功能描述和一些文档(或指向文档的链接)。
您应注意仅将源文件置于版本控制之下。例如,不应对构建系统生成的工件进行版本控制。您可以通过将此类文件添加到 .gitignore 文件中来指示 Git 忽略此类文件。
如果您使用的是 sbt,请确保您的存储库有一个 project/build.properties 文件,其中指明要使用的 sbt 版本,以便处理您的存储库的人员(或工具)将自动使用正确的 sbt 版本。
设置持续集成
设置持续集成 (CI) 服务器的第一个原因是在拉取请求上系统地运行测试。免费用于开源项目的 CI 服务器示例包括 GitHub Actions、Travis CI、Drone 或 AppVeyor。
我们的示例使用 GitHub Actions。此功能在 GitHub 存储库中默认启用。您可以在存储库的设置选项卡的操作部分中验证是否如此。如果选中禁用所有操作,则表示未启用操作,您可以通过选择允许所有操作、仅允许本地操作或允许选择操作来激活它们。
启用操作后,您可以创建一个工作流定义文件。工作流是一个自动化过程,由一个或多个作业组成。作业是一组按顺序执行的步骤,它们在同一执行器上执行。步骤是一项可以运行命令的单独任务;步骤可以是操作或 shell 命令。操作是工作流中最小的构建块,可以重复使用社区操作或定义新操作。
要创建工作流,请在存储库中的 .github/workflows/
目录中创建一个 yaml 文件,例如 .github/workflows/ci.yml
,其中包含以下内容
name: Continuous integration
on: push
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3 # Retrieve the content of the repository
- uses: actions/setup-java@v3 # Set up a jdk
with:
distribution: temurin
java-version: 8
cache: sbt # Cache the artifacts downloaded by sbt accross CI runs
- name: unit tests # Custom action consisting of a shell command
run: sbt +test
此工作流称为 持续集成,它将在每次将一个或多个提交推送到存储库时运行。它仅包含一个名为 ci 的作业,该作业将在 Ubuntu 运行器上运行,并且由三个操作组成。操作 setup-java
安装 JDK 并缓存 sbt 下载的库依赖项,以便每次运行 CI 时不再下载它们。
然后,作业运行 sbt +test
,该作业加载 project/build.properties
中指定的 sbt 版本,并使用 build.sbt
文件中定义的 Scala 版本运行项目测试。
上述工作流将在任何分支的任何推送中运行。你可以指定分支或添加更多触发器,例如拉取请求、版本、标签或计划。有关工作流触发器的更多信息,请参见 此处。而 setup-java
操作托管 在此存储库中。
作为参考,以下是我们的完整 工作流示例文件。
发布版本
大多数构建工具通过在公共存储库(例如 Maven Central)中查找第三方依赖项来解析它们。这些存储库托管库二进制文件以及其他信息,例如库作者、开源许可证和库本身的依赖项。库的每个版本都由 groupId
、artifactId
和 version
编号标识。例如,考虑以下依赖项(以 sbt 的语法编写)
"org.slf4j" % "slf4j-simple" % "1.7.25"
它的 groupId
是 org.slf4j
,它的 artifactId
是 slf4j-simple
,它的 version
是 1.7.25
。
在本文件中,我们展示如何发布 Maven Central 存储库。此过程需要拥有 Sonatype 帐户和 PGP 密钥对来对二进制文件进行签名。
创建 Sonatype 帐户和项目
按照 OSSRH 指南 中给出的说明创建一个新的 Sonatype 帐户(除非您已经有一个帐户),并 创建一个新的项目工单。后一步是定义您将发布到的 groupId
。您可以使用您已经拥有的域名,否则一种常见做法是使用 io.github.(username)
(其中 (username)
用您的 GitHub 用户名替换)。
此步骤仅需针对您想要拥有的每个 groupId
执行一次。
创建 PGP 密钥对
Sonatype 要求 您使用 PGP 对已发布的文件进行签名。按照 此处 的说明生成密钥对,并将您的公钥分发到密钥服务器。
此步骤仅需针对每个人执行一次。
设置您的项目
如果您使用 sbt,我们建议使用 sbt-sonatype 和 sbt-pgp 插件来发布您的工件。将以下依赖项添加到您的 project/plugins.sbt
文件
addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.21")
addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.2.1")
并通过定义以下设置确保您的构建满足 Sonatype 要求
// used as `artifactId`
name := "library-example"
// used as `groupId`
organization := "ch.epfl.scala"
// open source licenses that apply to the project
licenses := Seq("APL2" -> url("https://www.apache.org/licenses/LICENSE-2.0.txt"))
description := "A library that does nothing useful"
import xerial.sbt.Sonatype._
sonatypeProjectHosting := Some(GitHubHosting("scalacenter", "library-example", "[email protected]"))
// publish to the sonatype repository
publishTo := sonatypePublishToBundle.value
将您的 Sonatype 凭据放入 $HOME/.sbt/1.0/sonatype.sbt
文件中
credentials += Credentials("Sonatype Nexus Repository Manager",
"oss.sonatype.org",
"(Sonatype user name)",
"(Sonatype password)")
(将您的实际用户名和密码放入 (Sonatype user name)
和 (Sonatype password)
的位置)
切勿将此文件检入版本控制。
最后,我们建议使用 sbt-dynver 插件来设置您的发布的版本号。将以下依赖项添加到您的 project/plugins.sbt
文件
addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.0.1")
并确保您的构建不定义 version
设置。
剪切发布
使用此设置,剪切发布的过程如下。
创建一个 Git 标记,其名称以小写 v
开头,后跟版本号
$ git tag v0.1.0
此标记由 sbt-dynver
用于计算发布的版本(在此示例中为 0.1.0
)。
使用 publishSigned
sbt 任务将您的工件部署到中央存储库
$ sbt publishSigned
sbt-sonatype
将打包您的项目并询问您的 PGP 密码以使用您的 PGP 密钥对文件进行签名。然后,它将使用您的帐户凭据将文件上传到 Sonatype。当任务完成后,您可以在 Nexus Repository Manager 中检查工件(在侧边菜单中的“暂存存储库”下 − 如果您没有看到它,请确保您已登录)。
最后,使用 sonatypeRelease
sbt 任务执行发布
$ sbt sonatypeRelease
设置持续发布
上面描述的发布过程有一些缺点
- 它需要运行三个命令,
- 它不能保证在发布库时库处于稳定状态(即,某些测试可能会失败),
- 如果您在团队中工作,每个贡献者都必须设置自己的 PGP 密钥对,并且必须拥有对项目的
groupId
具有访问权限的 Sonatype 凭据。
持续发布通过将发布过程委托给 CI 服务器来解决这些问题。它的工作原理如下:任何对存储库具有写访问权限的贡献者都可以通过推送 Git 标记来发布版本,CI 服务器首先检查测试是否通过,然后运行发布命令。
我们通过在文件 project/plugins.sbt
中用 sbt-ci-release
替换插件 sbt-pgp
、sbt-sonatype
和 sbt-dynver
来实现这一点
其余部分展示了如何在 Sonatype 上为持续发布设置 GitHub Actions。您可以在 sbt-ci-release 插件文档中找到有关 Travis CI 的说明。
设置 CI 服务器
您必须向 CI 服务器提供您的 Sonatype 帐户凭据以及您的 PGP 密钥对。幸运的是,可以通过使用 CI 服务器的秘密管理系统安全地提供此信息。
导出您的 Sonatype 帐户凭据
为您的 Sonatype 帐户凭据创建两个 GitHub 加密秘密:SONATYPE_USERNAME
和 SONATYPE_PASSWORD
。为此,请转到存储库的“设置”选项卡,然后在左侧面板中选择“秘密”。然后,您可以使用按钮“新建存储库秘密”打开秘密创建菜单,您将在其中输入秘密的名称及其内容。
存储库秘密允许我们安全地存储机密信息,并将其公开给 Actions 工作流,而不会冒将其提交到 git 历史记录的风险。
导出您的 PGP 密钥对
要导出您的 PGP 密钥对,您首先需要知道它的标识符。使用以下命令列出您的 PGP 密钥
$ gpg --list-secret-keys
/home/julien/.gnupg/secring.gpg
-------------------------------
sec 2048R/BE614499 2016-08-12
uid Julien Richard-Foy <[email protected]>
在我的案例中,我有一对密钥,其 ID 为 BE614499
。
然后
- 创建一个新 Secret,其中包含名为
PGP_PASSPHRASE
的 PGP 密钥的密码。 - 创建一个新 Secret,其中包含名为
PGP_SECRET
的私钥的 base64 编码密钥。可以通过运行以下命令获取编码密钥# macOS gpg --armor --export-secret-keys $LONG_ID | base64 # Ubuntu (assuming GNU base64) gpg --armor --export-secret-keys $LONG_ID | base64 -w0 # Arch gpg --armor --export-secret-keys $LONG_ID | base64 | sed -z 's;\n;;g' # FreeBSD (assuming BSD base64) gpg --armor --export-secret-keys $LONG_ID | base64 # Windows gpg --armor --export-secret-keys %LONG_ID% | openssl base64
- 将您的公钥签名发布到公有服务器,例如 http://keyserver.ubuntu.com:11371。您可以通过运行以下命令获取签名
# macOS and linux gpg --armor --export $LONG_ID # Windows gpg --armor --export %LONG_ID%
(用您的密钥 ID 替换
(key ID)
)
从 CI 服务器发布
在 GitHub Actions 中,您可以定义一个工作流,以便在推送以“v”开头的标签时发布库
env
语句通过环境变量向发布过程公开您之前定义的密钥。
剪切发布
只需推送一个 Git 标签
$ git tag v0.2.0
$ git push origin v0.2.0
这将触发工作流,最终调用 sbt ci-release
,它将执行 publishSigned
,然后执行 sonatypeRelease
。
交叉发布
如果您编写了一个库,您可能希望它可以从多个 Scala 主要版本(例如 2.12.x、2.13.x、3.x 等)中使用。
在 build.sbt
文件中,在 crossScalaVersions
设置中定义您希望支持的版本
crossScalaVersions := Seq("3.3.0", "2.13.12", "2.12.18")
scalaVersion := crossScalaVersions.value.head
第二行使 sbt 默认使用 crossScalaVersions
的第一个 Scala 版本。CI 作业将使用您构建定义中的所有 Scala 版本。
发布在线文档
文档的一个重要属性是代码示例应按其呈现方式编译并表现。有许多方法可确保此属性成立。一种方法是 mdoc 支持的,即实际评估代码示例并在生成的文档中写入其评估结果。另一种方法是嵌入来自真实模块或示例的源代码片段。
sbt-site 插件可以帮助你组织、构建和预览你的文档。它与其他 sbt 插件很好地集成,用于生成文档内容或将生成的文档发布到 Web 服务器。
最后,在线发布文档的一个简单解决方案是使用 GitHub Pages 服务,该服务可自动用于每个 GitHub 存储库。sbt-ghpages 插件可以自动将 sbt-site 上传到 GitHub Pages。
创建文档站点
在此示例中,我们选择使用 Paradox,因为它在 JVM 上运行,因此不需要在你的系统上设置另一个 VM(与大多数其他文档生成器形成对比,它们基于 Ruby、Node.js 或 Python)。
要安装 Paradox 和 sbt-site,请将以下行添加到 project/plugins.sbt
文件
addSbtPlugin("com.github.sbt" % "sbt-site-paradox" % "1.5.0")
然后将以下配置添加到 build.sbt
文件
ParadoxSitePlugin
提供了一个任务 makeSite
,该任务使用 Paradox 生成一个网站,而 SitePreviewPlugin
在处理网站内容时提供便捷的任务,以便在你的浏览器中预览结果。第二行是可选的,它定义了网站源文件的位置。在我们的示例中,位于 src/documentation
中。
在 src/documentation/index.md
文件中添加你的文档入口点。典型的文档入口点使用库名称作为标题,显示一个简短的句子来描述库的用途,以及一个代码片段,用于将库添加到构建定义中
请注意,在我们的示例中,我们依赖于变量替换机制,以便在文档中注入正确的版本号,这样我们不必在每次发布新版本时都更新文档的该部分。
我们的示例还包括一个 @@@index
指令,该指令定义了文档内容的组织方式。在我们的示例中,该文档包含两页,第一页提供了快速教程,以便熟悉该库,第二页提供了更详细的信息。
sbt-site 插件提供了一个方便的 previewAuto
任务,用于在本地提供生成的文档,以便您可以查看文档的外观,并在编辑文档时重新生成文档
sbt:library-example> previewAuto
Embedded server listening at
https://127.0.0.1:4000
Press any key to stop.
浏览 https://localhost:4000 URL 查看结果
包含代码示例
本节展示了确保包含在文档中的代码示例按其展示方式编译和运行的两种方法。
使用 Markdown 预处理器
一种方法是使用 Markdown 预处理器,例如 mdoc。这些工具会读取您的 Markdown 源文件,搜索代码围栏,评估它们(如果不编译则抛出错误),并生成 Markdown 文件的副本,其中代码围栏已更新,还包括评估 Scala 表达式的结果。
嵌入代码片段
另一种方法是嵌入 Scala 源文件片段,这些片段是您构建编译的模块的一部分。例如,给定文件 src/test/ch/epfl/scala/Usage.scala
中的以下测试
package ch.epfl.scala
import scalaprops.{Property, Scalaprops}
object Usage extends Scalaprops {
val testDoNothing =
// #do-nothing
Property.forAll { x: Int =>
Example.doNothing(x) == x
}
// #do-nothing
}
package ch.epfl.scala
import scalaprops.{Property, Scalaprops}
object Usage extends Scalaprops:
val testDoNothing =
// #do-nothing
Property.forAll: (x: Int) =>
Example.doNothing(x) == x
// #do-nothing
end Usage
您可以使用 @@snip
Paradox 指令,用 #do-nothing
标识符包围的片段嵌入到 src/documentation/reference.md
文件中,如下所示
生成的文档如下所示
包含 API 文档
从您的文档网站链接到 API 文档(Scaladoc)也可能很有用。
这可以通过将以下行添加到您的 build.sbt
来实现
SiteScaladocPlugin
由 sbt-site
提供,并将 API 文档包含在生成的网站中。第二行定义 API 文档应发布在 /api
基本 URL 中,第三行使此信息可供 Paradox 使用。
然后,您可以使用 @scaladoc
Paradox 指令来包含指向库中特定符号的 API 文档的链接
@scaladoc
指令将生成指向 /api/ch/epfl/scala/Example$.html
页面的链接。
发布文档
将 sbt-ghpages
插件添加到您的 project/plugins.sbt
addSbtPlugin("com.github.sbt" % "sbt-ghpages" % "0.8.0")
并将以下配置添加到您的 build.sbt
在您的项目存储库中创建一个 gh-pages
分支,如 sbt-ghpages 文档 中所述。
最后,通过运行 ghpagesPushSite
sbt 任务发布你的网站
sbt:library-example> ghpagesPushSite
[info] Cloning into '.'...
[info] [gh-pages 2e7f426] updated site
[info] 83 files changed, 8059 insertions(+)
[info] create mode 100644 .nojekyll
[info] create mode 100644 api/ch/epfl/index.html
…
[info] To [email protected]:scalacenter/library-example.git
[info] 2d62539..2e7f426 gh-pages -> gh-pages
[success] Total time: 9 s, completed Jan 22, 2019 10:55:15 AM
你的网站应该在线显示在 https://(organization).github.io/(project)
。在我们的案例中,你可以在 https://scalacenter.github.io/library-example/ 浏览它。
持续发布
你可以扩展 .github/workflows/publish.yml
以自动将文档发布到 GitHub 页面。为此,添加另一个作业
# .github/workflows/publish.yml
name: Continuous publication
jobs:
release: # The release job is not changed, you can find it above
publishSite:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 8
cache: sbt
- name: Generate site
run: sbt makeSite
- uses: JamesIves/[email protected]
with:
branch: gh-pages
folder: target/site
像往常一样,通过推送 Git 标记来发布版本。CI 服务器将运行测试,发布二进制文件并更新在线文档。
欢迎贡献者
本部分将为你提供建议,帮助你让更多人参与你的项目。
CONTRIBUTING.md
向你的存储库添加一个 CONTRIBUTING.md
文件,回答以下问题:如何构建项目?需要遵循哪些编码实践?测试在哪里,如何运行它们?
作为参考,你可以阅读我们 CONTRIBUTING.md
文件 的最小示例。
问题标签
我们建议你标记你的项目问题,以便潜在的贡献者能够快速了解问题的范围(例如,“文档”、“核心”等),其难度级别(例如,“好的第一个问题”、“高级”等),或其优先级(例如,“阻碍因素”、“很高兴有”等)。
代码格式化
查看大量代码样式更改稀释了实质性更改的拉取请求可能会令人沮丧。你可以通过使用代码格式化程序强制所有贡献者遵循特定的代码样式来避免该问题。
例如,要使用 scalafmt,将以下行添加到你的 project/plugins.sbt
文件中
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.2")
在 CONTRIBUTING.md
文件中,提到你使用了该代码格式化程序,并鼓励用户使用其编辑器的“保存时格式化”功能。
在你的 .github/workflows/ci.yml
文件中,添加一个检查代码是否已正确格式化的步骤
# .github/workflows/ci.yml
# The three periods `...` indicate the parts of file that do not change
# from the snippets above and they are omitted for brevity
jobs:
ci:
# ...
steps:
# ...
- name: Code style
run: sbt scalafmtCheck
演化
从用户角度来看,升级到库的新版本应该是一个平滑的过程。甚至可能是一个“非事件”。
重大更改和迁移步骤应得到彻底记录,我们建议遵循语义版本控制策略。
MiMa工具可以帮助您检查是否违反了此版本控制策略。在project/plugins.sbt
文件中,使用以下内容将sbt-mima-plugin
添加到您的构建中
addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.2")
在build.sbt
中按如下方式配置它
mimaPreviousArtifacts := previousStableVersion.value.map(organization.value %% name.value % _).toSet
最后,在.github/workflows/ci.yml
文件中,将以下步骤添加到Continuous integration
工作流的ci
作业中
# .github/workflows/ci.yml
# The three periods `...` indicate the parts of file that do not change
# from the snippets above and they are omitted for brevity
# ...
jobs:
ci:
# ...
steps:
# ...
- name: Binary compatibility
run: sbt mimaReportBinaryIssues
这将检查拉取请求是否会对与先前稳定版本二进制不兼容的更改。
我们建议使用以下 Git 工作流:main
分支始终接收针对下一个主要版本的拉取请求(因此,通过将mimaPreviousArtifacts
值设置为Set.empty
来禁用二进制兼容性检查),并且每个主要版本N
都有一个对应的N.x
分支(例如,1.x
、2.x
等)分支,其中启用了二进制兼容性检查。