Skip to content

Commit 7265716

Browse files
Merge pull request #7820 from HenrikJannsen/improve-release-process-tasks
Add Gradle release artifact signing tasks
2 parents 47ed5cc + 94bfa32 commit 7265716

3 files changed

Lines changed: 251 additions & 13 deletions

File tree

build.gradle

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,32 @@ def canonicalPath = { File file ->
700700
rootProject.relativePath(file).replace(File.separatorChar, '/' as char)
701701
}
702702

703+
def normalizeReleaseVersion = { String rawReleaseVersion ->
704+
if (rawReleaseVersion == null || rawReleaseVersion.trim().empty) {
705+
throw new GradleException('Missing required -PreleaseVersion=<version>, for example -PreleaseVersion=1.10.0')
706+
}
707+
708+
def releaseVersion = rawReleaseVersion.trim()
709+
if (releaseVersion.startsWith('v')) {
710+
releaseVersion = releaseVersion.substring(1)
711+
}
712+
if (!(releaseVersion ==~ /\d+\.\d+\.\d+([.-][A-Za-z0-9_.-]+)?/)) {
713+
throw new GradleException("Invalid release version '${rawReleaseVersion}'. Expected a version such as 1.10.0 or v1.10.0.")
714+
}
715+
716+
releaseVersion
717+
}
718+
719+
def resolveGpgExecutable = {
720+
def configuredGpgExecutable = providers.gradleProperty('gpgExecutable').orNull ?:
721+
providers.environmentVariable('GPG_EXECUTABLE').orNull
722+
configuredGpgExecutable ?: [
723+
'/opt/homebrew/bin/gpg',
724+
'/usr/local/bin/gpg',
725+
'/usr/bin/gpg'
726+
].find { new File(it).canExecute() } ?: 'gpg'
727+
}
728+
703729
def enabledJarTasks = {
704730
allprojects.collectMany { project ->
705731
project.tasks.withType(Jar).findAll { it.enabled }
@@ -2709,6 +2735,184 @@ tasks.register('verifyReleaseBuild') {
27092735
}
27102736
}
27112737

2738+
tasks.register('createReleaseJarTxt') {
2739+
group = 'distribution'
2740+
description = 'Creates Bisq-<version>.jar.txt from the four platform jar SHA-256 files.'
2741+
2742+
def releaseVersionProperty = providers.gradleProperty('releaseVersion')
2743+
def releaseDirProperty = providers.gradleProperty('releaseDir')
2744+
def jarSha256Inputs = [
2745+
[property: 'macosX86_64JarSha256', label: 'macOS Intel'],
2746+
[property: 'macosAarch64JarSha256', label: 'macOS Apple Silicon'],
2747+
[property: 'linuxJarSha256', label: 'Linux'],
2748+
[property: 'windowsJarSha256', label: 'Windows']
2749+
]
2750+
2751+
inputs.property('releaseVersion', releaseVersionProperty.orElse(''))
2752+
inputs.property('releaseDir', releaseDirProperty.orElse(''))
2753+
jarSha256Inputs.each { inputSpec ->
2754+
inputs.property(inputSpec.property, providers.gradleProperty(inputSpec.property).orElse(''))
2755+
}
2756+
outputs.upToDateWhen { false }
2757+
2758+
doLast {
2759+
def releaseVersion = normalizeReleaseVersion(releaseVersionProperty.orNull)
2760+
def rawReleaseDir = releaseDirProperty.orNull
2761+
if (rawReleaseDir == null || rawReleaseDir.trim().empty) {
2762+
throw new GradleException('Missing required -PreleaseDir=<directory>, for example -PreleaseDir=/Users/ben/Documents/v1.10.0')
2763+
}
2764+
2765+
def releaseDir = file(rawReleaseDir.trim())
2766+
if (!releaseDir.exists() && !releaseDir.mkdirs()) {
2767+
throw new GradleException("Could not create release directory: ${releaseDir}")
2768+
}
2769+
if (!releaseDir.directory) {
2770+
throw new GradleException("Release directory is not a directory: ${releaseDir}")
2771+
}
2772+
2773+
def sourceFiles = jarSha256Inputs.collect { inputSpec ->
2774+
def rawPath = providers.gradleProperty(inputSpec.property).orNull
2775+
if (rawPath == null || rawPath.trim().empty) {
2776+
throw new GradleException("Missing required -P${inputSpec.property}=<path> for the ${inputSpec.label} jar SHA-256 file.")
2777+
}
2778+
2779+
def sourceFile = file(rawPath.trim())
2780+
if (!sourceFile.file) {
2781+
throw new GradleException("Missing ${inputSpec.label} jar SHA-256 file: ${sourceFile}")
2782+
}
2783+
sourceFile
2784+
}
2785+
2786+
def outputFile = new File(releaseDir, "Bisq-${releaseVersion}.jar.txt")
2787+
outputFile.withOutputStream { output ->
2788+
sourceFiles.each { sourceFile ->
2789+
def bytes = sourceFile.bytes
2790+
output.write(bytes)
2791+
if (bytes.length == 0 || bytes[bytes.length - 1] != ((byte) 10)) {
2792+
output.write('\n'.getBytes(StandardCharsets.UTF_8.name()))
2793+
}
2794+
}
2795+
}
2796+
2797+
logger.lifecycle("Wrote ${outputFile.absolutePath}")
2798+
logger.lifecycle("Aggregated jar checksum sources:")
2799+
sourceFiles.each { sourceFile ->
2800+
logger.lifecycle(" - ${sourceFile.absolutePath}")
2801+
}
2802+
}
2803+
}
2804+
2805+
tasks.register('signReleaseArtifacts') {
2806+
group = 'distribution'
2807+
description = 'Signs release binary artifacts and Bisq-<version>.jar.txt in -PreleaseDir with detached armored GPG signatures.'
2808+
2809+
def releaseVersionProperty = providers.gradleProperty('releaseVersion')
2810+
def releaseDirProperty = providers.gradleProperty('releaseDir')
2811+
def gpgUserProperty = providers.gradleProperty('gpgUser')
2812+
def bisqGpgUserProperty = providers.gradleProperty('bisqGpgUser')
2813+
def bisqGpgUserEnvironment = providers.environmentVariable('BISQ_GPG_USER')
2814+
def gpgExecutableProperty = providers.gradleProperty('gpgExecutable')
2815+
def gpgExecutableEnvironment = providers.environmentVariable('GPG_EXECUTABLE')
2816+
def binaryExtensions = ['dmg', 'deb', 'rpm', 'exe', 'zip', 'pkg', 'msi', 'tar.gz', 'tgz']
2817+
2818+
inputs.property('releaseVersion', releaseVersionProperty.orElse(''))
2819+
inputs.property('releaseDir', releaseDirProperty.orElse(''))
2820+
inputs.property('gpgUser', gpgUserProperty.orElse(bisqGpgUserProperty).orElse(bisqGpgUserEnvironment).orElse(''))
2821+
inputs.property('gpgExecutable', gpgExecutableProperty.orElse(gpgExecutableEnvironment).orElse(''))
2822+
outputs.upToDateWhen { false }
2823+
2824+
doLast {
2825+
def releaseVersion = normalizeReleaseVersion(releaseVersionProperty.orNull)
2826+
def rawReleaseDir = releaseDirProperty.orNull
2827+
if (rawReleaseDir == null || rawReleaseDir.trim().empty) {
2828+
throw new GradleException('Missing required -PreleaseDir=<directory>, for example -PreleaseDir=/Users/ben/Documents/v1.10.0')
2829+
}
2830+
2831+
def gpgUser = gpgUserProperty.orNull ?:
2832+
bisqGpgUserProperty.orNull ?:
2833+
bisqGpgUserEnvironment.orNull
2834+
if (gpgUser == null || gpgUser.trim().empty) {
2835+
throw new GradleException('Missing required -PgpgUser=<key-id-or-email>. You can also use -PbisqGpgUser=<key-id-or-email> or BISQ_GPG_USER.')
2836+
}
2837+
gpgUser = gpgUser.trim()
2838+
2839+
def releaseDir = file(rawReleaseDir.trim())
2840+
if (!releaseDir.directory) {
2841+
throw new GradleException("Release directory is not a directory: ${releaseDir}")
2842+
}
2843+
2844+
def jarTxtFileName = "Bisq-${releaseVersion}.jar.txt"
2845+
def jarTxtFile = new File(releaseDir, jarTxtFileName)
2846+
if (!jarTxtFile.file) {
2847+
throw new GradleException(
2848+
"Missing ${jarTxtFileName} in ${releaseDir}. Create it with the createReleaseJarTxt task or desktop/package/macosx/finalize.sh before signing."
2849+
)
2850+
}
2851+
2852+
def releaseDirFiles = releaseDir.listFiles()
2853+
if (releaseDirFiles == null) {
2854+
throw new GradleException("Cannot list release directory: ${releaseDir}. Check that it is readable.")
2855+
}
2856+
2857+
def artifacts = releaseDirFiles
2858+
.findAll { candidate ->
2859+
def signableFile = candidate.file &&
2860+
!candidate.name.startsWith('.') &&
2861+
!candidate.name.endsWith('.asc')
2862+
def lowerName = candidate.name.toLowerCase(Locale.ROOT)
2863+
signableFile &&
2864+
(candidate.name == jarTxtFileName ||
2865+
binaryExtensions.any { extension -> lowerName.endsWith(".${extension}") })
2866+
}
2867+
.sort { left, right -> left.name <=> right.name }
2868+
2869+
if (artifacts.empty) {
2870+
throw new GradleException("No release artifacts found to sign in ${releaseDir}")
2871+
}
2872+
2873+
def gpgExecutable = resolveGpgExecutable()
2874+
def failureDetails = { result ->
2875+
[result.stderr, result.stdout]
2876+
.findAll { it != null && !it.trim().empty }
2877+
.join('\n')
2878+
.trim()
2879+
}
2880+
2881+
logger.lifecycle("Signing ${artifacts.size()} release artifact(s) in ${releaseDir.absolutePath}")
2882+
artifacts.each { artifact ->
2883+
def signatureFile = new File(artifact.parentFile, "${artifact.name}.asc")
2884+
def signResult = execResult([
2885+
gpgExecutable,
2886+
'--yes',
2887+
'--digest-algo', 'SHA256',
2888+
'--local-user', gpgUser,
2889+
'--output', signatureFile.absolutePath,
2890+
'--detach-sig',
2891+
'--armor',
2892+
artifact.absolutePath
2893+
])
2894+
if (signResult.exitValue != 0) {
2895+
throw new GradleException("Failed to sign ${artifact.name}: ${failureDetails(signResult)}")
2896+
}
2897+
if (!signatureFile.file || signatureFile.length() == 0) {
2898+
throw new GradleException("GPG did not create a non-empty signature file for ${artifact.name}: ${signatureFile}")
2899+
}
2900+
2901+
def verifyResult = execResult([
2902+
gpgExecutable,
2903+
'--verify',
2904+
signatureFile.absolutePath,
2905+
artifact.absolutePath
2906+
])
2907+
if (verifyResult.exitValue != 0) {
2908+
throw new GradleException("Failed to verify ${signatureFile.name}: ${failureDetails(verifyResult)}")
2909+
}
2910+
2911+
logger.lifecycle("Signed and verified ${artifact.name} -> ${signatureFile.name}")
2912+
}
2913+
}
2914+
}
2915+
27122916
tasks.register('verifyGithubReleaseReadiness') {
27132917
group = 'verification'
27142918
description = 'Manually verifies GitHub release assets, Bisq download URLs, versions, and signer keys for -PreleaseVersion=<version>.'
@@ -2898,6 +3102,7 @@ tasks.register('verifyGithubReleaseReadiness') {
28983102
"Bisq-aarch64-${releaseVersion}.dmg".toString(),
28993103
"Bisq-aarch64-${releaseVersion}.dmg.asc".toString(),
29003104
"Bisq-${releaseVersion}.jar.txt".toString(),
3105+
"Bisq-${releaseVersion}.jar.txt.asc".toString(),
29013106
"Bisq-64bit-${releaseVersion}.deb".toString(),
29023107
"Bisq-64bit-${releaseVersion}.deb.asc".toString(),
29033108
"Bisq-64bit-${releaseVersion}.rpm".toString(),
@@ -2923,6 +3128,7 @@ tasks.register('verifyGithubReleaseReadiness') {
29233128
"Bisq-64bit-${releaseVersion}.exe".toString(),
29243129
"Bisq-64bit-${releaseVersion}.exe.asc".toString(),
29253130
"Bisq-${releaseVersion}.jar.txt".toString(),
3131+
"Bisq-${releaseVersion}.jar.txt.asc".toString(),
29263132
'signingkey.asc'
29273133
].sort()
29283134

desktop/package/macosx/finalize.sh

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/bin/bash
2+
set -e
23

34
cd ../../../
45

@@ -19,9 +20,9 @@ macos_aarch64=${BISQ_MACOS_AARCH64_PATH:-$vmPath/vm_shared_macosx_aarch64}
1920

2021
deployDir=deploy
2122

22-
rm -r $target_dir
23+
rm -rf "$target_dir"
2324

24-
mkdir -p $target_dir
25+
mkdir -p "$target_dir"
2526

2627
# make sure the releases are ready
2728
./gradlew cli:build
@@ -86,12 +87,17 @@ cp "$win64/$exe" "$target_dir/$exe64"
8687

8788
cli="bisq-cli-$version.zip"
8889
daemon="bisq-daemon-$version.zip"
90+
jar_txt="Bisq-$version.jar.txt"
8991

90-
# create file with jar signatures
92+
# create file with jar checksums
9193
cat "$macos_x86_64/desktop-$version-all-mac-x86_64.jar.SHA-256" \
9294
"$macos_aarch64/desktop-$version-all-mac-aarch64.jar.SHA-256" \
9395
"$linux64/desktop-$version-all-linux.jar.SHA-256" \
94-
"$win64/desktop-$version-all-win.jar.SHA-256" > "$target_dir/Bisq-$version.jar.txt"
96+
"$win64/desktop-$version-all-win.jar.SHA-256" > "$target_dir/$jar_txt"
97+
if [[ ! -s "$target_dir/$jar_txt" ]]; then
98+
echo "Missing or empty jar checksum file: $target_dir/$jar_txt" >&2
99+
exit 1
100+
fi
95101

96102
cd "$script_working_directory/$target_dir" || exit 1
97103

@@ -103,6 +109,7 @@ gpg --digest-algo SHA256 --local-user "$BISQ_GPG_USER" --output "$rpm64.asc" --d
103109
gpg --digest-algo SHA256 --local-user "$BISQ_GPG_USER" --output "$exe64.asc" --detach-sig --armor "$exe64"
104110
gpg --digest-algo SHA256 --local-user "$BISQ_GPG_USER" --output "$cli.asc" --detach-sig --armor "$cli"
105111
gpg --digest-algo SHA256 --local-user "$BISQ_GPG_USER" --output "$daemon.asc" --detach-sig --armor "$daemon"
112+
gpg --digest-algo SHA256 --local-user "$BISQ_GPG_USER" --output "$jar_txt.asc" --detach-sig --armor "$jar_txt"
106113

107114
echo Verify signatures
108115
gpg --digest-algo SHA256 --verify $dmg_x86_64{.asc*,}
@@ -112,8 +119,9 @@ gpg --digest-algo SHA256 --verify $rpm64{.asc*,}
112119
gpg --digest-algo SHA256 --verify $exe64{.asc*,}
113120
gpg --digest-algo SHA256 --verify $cli{.asc*,}
114121
gpg --digest-algo SHA256 --verify $daemon{.asc*,}
122+
gpg --digest-algo SHA256 --verify $jar_txt{.asc*,}
115123

116-
mkdir $win64/$version
117-
cp -r . $win64/$version
124+
mkdir -p "$win64/$version"
125+
cp -r . "$win64/$version"
118126

119127
open "./desktop/releases/$version"

docs/release-process.md

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ Build output expected:
139139
Build output expected:
140140

141141
1. `Bisq-${NEW_VERSION}.exe` Windows installer
142-
2. `desktop-${NEW_VERSION}-all-windows.jar.SHA-256` sha256 sum of fat jar
142+
2. `desktop-${NEW_VERSION}-all-win.jar.SHA-256` sha256 sum of fat jar
143143

144144
* Install and run generated package
145145

@@ -162,12 +162,36 @@ Build output expected:
162162
6. `Bisq-aarch64-${NEW_VERSION}.dmg` macOS Apple Silicon installer
163163
7. `Bisq-aarch64-${NEW_VERSION}.dmg.asc` Signature for macOS Apple Silicon installer
164164
8. `Bisq-${NEW_VERSION}.jar.txt` Aggregated SHA-256 file for macOS, Linux, and Windows jar libraries
165-
9. `Bisq-64bit-${NEW_VERSION}.deb` Debian package
166-
10. `Bisq-64bit-${NEW_VERSION}.deb.asc` Signature for Debian package
167-
11. `Bisq-64bit-${NEW_VERSION}.rpm` Red Hat based distro package
168-
12. `Bisq-64bit-${NEW_VERSION}.rpm.asc` Signature for Red Hat based distro package
169-
13. `Bisq-64bit-${NEW_VERSION}.exe` Windows installer
170-
14. `Bisq-64bit-${NEW_VERSION}.exe.asc` Signature for Windows installer
165+
9. `Bisq-${NEW_VERSION}.jar.txt.asc` Signature for aggregated SHA-256 file
166+
10. `Bisq-64bit-${NEW_VERSION}.deb` Debian package
167+
11. `Bisq-64bit-${NEW_VERSION}.deb.asc` Signature for Debian package
168+
12. `Bisq-64bit-${NEW_VERSION}.rpm` Red Hat based distro package
169+
13. `Bisq-64bit-${NEW_VERSION}.rpm.asc` Signature for Red Hat based distro package
170+
14. `Bisq-64bit-${NEW_VERSION}.exe` Windows installer
171+
15. `Bisq-64bit-${NEW_VERSION}.exe.asc` Signature for Windows installer
172+
173+
If you need to create and sign the final release directory manually, use the Gradle tasks below. The
174+
`Bisq-${NEW_VERSION}.jar.txt` file is the concatenation of the four platform jar SHA-256 files produced by
175+
`./gradlew packageInstallers`; those files are in the VM shared folders / package output directories listed in the
176+
macOS, Linux, and Windows build sections above. When using `finalize.sh`, the generated file is written to
177+
`desktop/releases/${NEW_VERSION}/Bisq-${NEW_VERSION}.jar.txt`.
178+
179+
./gradlew createReleaseJarTxt \
180+
-PreleaseVersion=${NEW_VERSION} \
181+
-PreleaseDir=/path/to/final-release-dir \
182+
-PmacosX86_64JarSha256=/path/to/desktop-${NEW_VERSION}-all-mac-x86_64.jar.SHA-256 \
183+
-PmacosAarch64JarSha256=/path/to/desktop-${NEW_VERSION}-all-mac-aarch64.jar.SHA-256 \
184+
-PlinuxJarSha256=/path/to/desktop-${NEW_VERSION}-all-linux.jar.SHA-256 \
185+
-PwindowsJarSha256=/path/to/desktop-${NEW_VERSION}-all-win.jar.SHA-256
186+
187+
./gradlew signReleaseArtifacts \
188+
-PreleaseVersion=${NEW_VERSION} \
189+
-PreleaseDir=/path/to/final-release-dir \
190+
-PgpgUser=${BISQ_GPG_USER}
191+
192+
`signReleaseArtifacts` creates detached armored `.asc` signatures for release binaries (`.dmg`, `.deb`, `.rpm`,
193+
`.exe`, `.zip`, `.pkg`, `.msi`, `.tar.gz`, `.tgz`) and for `Bisq-${NEW_VERSION}.jar.txt`, then verifies every
194+
signature it created.
171195

172196
* Run an AV scan over all files on the Windows VM where the files got copied over.
173197

0 commit comments

Comments
 (0)