mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
ci: add coordinated release pipeline
This commit is contained in:
41
packaging/release/README.md
Normal file
41
packaging/release/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Coordinated releases
|
||||
|
||||
Only `.github/workflows/release.yml` responds to version tags. It verifies that
|
||||
the tag matches `version.properties` and points at the current `master`, then
|
||||
calls the native platform workflows in parallel.
|
||||
|
||||
The tag workflow runs only when the repository variable
|
||||
`RELEASE_PIPELINE_ENABLED` is exactly `true`. Leave it unset or set it to
|
||||
`false` to disable all coordinated releases, including Play uploads, without
|
||||
disabling release validation on pull requests.
|
||||
|
||||
Platform workflows upload private workflow artifacts. After every native build
|
||||
passes, the release pipeline:
|
||||
|
||||
1. stages the signed AAB as a draft on the configured Play closed-test track;
|
||||
2. downloads the universal APK signed by Play;
|
||||
3. verifies and assembles the public artifacts;
|
||||
4. generates checksums and GitHub build-provenance attestations;
|
||||
5. creates exactly one GitHub Release;
|
||||
6. updates the Homebrew cask.
|
||||
|
||||
Public GitHub Release assets are the DEB, RPM, notarized DMG, Sparkle appcast,
|
||||
Play-signed universal APK, checksum file, and release manifest.
|
||||
|
||||
The unsigned Microsoft `.msixupload` and upload-signed Android AAB remain
|
||||
private workflow artifacts. Partner Center submission stays manual until the
|
||||
first Microsoft Store release is certified. The Play release remains a draft
|
||||
on a closed-testing track; this pipeline cannot publish to production.
|
||||
|
||||
To release, first update and merge `version.properties`, including monotonic
|
||||
Android and Apple build numbers. Then create and push the matching tag:
|
||||
|
||||
```bash
|
||||
git tag -s v0.2.0 -m "VniDrop 0.2.0"
|
||||
git push origin v0.2.0
|
||||
```
|
||||
|
||||
The tag must point at the current `origin/master` commit. A failed run creates
|
||||
no GitHub Release; a rerun safely reuses an already-staged Play draft only when
|
||||
the version, configured track, draft status, and app-signing certificate all
|
||||
match.
|
||||
169
packaging/release/assemble-release.sh
Executable file
169
packaging/release/assemble-release.sh
Executable file
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_root="$(cd "$script_dir/../.." && pwd)"
|
||||
resolver="$repo_root/packaging/version/resolve-version.sh"
|
||||
input_dir="${VNIDROP_RELEASE_INPUT_DIR:-$repo_root/build/release/downloads}"
|
||||
output_dir="${VNIDROP_RELEASE_OUTPUT_DIR:-$repo_root/build/release/final}"
|
||||
source_commit="${GITHUB_SHA:-local}"
|
||||
source_tag="${GITHUB_REF_NAME:-v$("$resolver" product)}"
|
||||
|
||||
find_single() {
|
||||
local directory=$1
|
||||
local pattern=$2
|
||||
local label=$3
|
||||
local matches=()
|
||||
local match
|
||||
while IFS= read -r match; do
|
||||
matches+=("$match")
|
||||
done < <(find "$directory" -type f -name "$pattern" -print)
|
||||
[[ ${#matches[@]} == 1 ]] || {
|
||||
printf 'Expected exactly one %s under %s, found %s\n' \
|
||||
"$label" "$directory" "${#matches[@]}" >&2
|
||||
exit 1
|
||||
}
|
||||
printf '%s' "${matches[0]}"
|
||||
}
|
||||
|
||||
file_size() {
|
||||
if stat -c '%s' "$1" >/dev/null 2>&1; then
|
||||
stat -c '%s' "$1"
|
||||
else
|
||||
stat -f '%z' "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
verify_checksum_file() {
|
||||
local checksum_file=$1
|
||||
(
|
||||
cd "$(dirname "$checksum_file")"
|
||||
sha256sum --check "$(basename "$checksum_file")"
|
||||
)
|
||||
}
|
||||
|
||||
version="$("$resolver" product)"
|
||||
android_code="$("$resolver" android-code)"
|
||||
apple_build="$("$resolver" apple-build)"
|
||||
windows_package="$("$resolver" windows-package)"
|
||||
"$resolver" verify >/dev/null
|
||||
[[ $source_tag == "v$version" ]] || {
|
||||
printf 'Release tag %s does not match canonical version v%s\n' \
|
||||
"$source_tag" "$version" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
deb="$(find_single "$input_dir/deb" '*.deb' 'Debian package')"
|
||||
rpm="$(find_single "$input_dir/rpm" '*.rpm' 'RPM package')"
|
||||
dmg="$(find_single "$input_dir/macos" '*.dmg' 'macOS DMG')"
|
||||
appcast="$(find_single "$input_dir/macos" 'appcast.xml' 'Sparkle appcast')"
|
||||
play_apk="$(find_single "$input_dir/play" '*-play-universal.apk' 'Play-signed APK')"
|
||||
play_metadata="$(find_single "$input_dir/play" 'play-release.json' 'Play release metadata')"
|
||||
msix="$(find_single "$input_dir/windows" '*.msix' 'Windows MSIX')"
|
||||
msixupload="$(find_single "$input_dir/windows" '*.msixupload' 'Windows MSIX upload')"
|
||||
windows_metadata="$(find_single "$input_dir/windows" '*.build-info.json' 'Windows build metadata')"
|
||||
|
||||
[[ $(basename "$deb") == "vnidrop_${version}-1_amd64.deb" ]]
|
||||
[[ $(basename "$rpm") == "vnidrop-${version}-1.x86_64.rpm" ]]
|
||||
[[ $(basename "$dmg") == "VniDrop-${version}.dmg" ]]
|
||||
[[ $(basename "$play_apk") == "VniDrop-${version}-${android_code}-play-universal.apk" ]]
|
||||
[[ $(basename "$msix") == "VniDrop_${version}_x64.msix" ]]
|
||||
[[ $(basename "$msixupload") == "VniDrop_${version}_x64.msixupload" ]]
|
||||
|
||||
deb_checksum="$(find_single "$input_dir/deb" '*.sha256' 'Debian checksum')"
|
||||
rpm_checksum="$(find_single "$input_dir/rpm" '*.sha256' 'RPM checksum')"
|
||||
windows_checksums="$(find_single "$input_dir/windows" 'SHA256SUMS' 'Windows checksums')"
|
||||
play_checksums="$(find_single "$input_dir/play" 'SHA256SUMS' 'Play APK checksums')"
|
||||
verify_checksum_file "$deb_checksum"
|
||||
verify_checksum_file "$rpm_checksum"
|
||||
verify_checksum_file "$windows_checksums"
|
||||
verify_checksum_file "$play_checksums"
|
||||
|
||||
[[ $(jq -r '.releaseStatus' "$play_metadata") == draft ]]
|
||||
[[ $(jq -r '.releaseName' "$play_metadata") == "$version" ]]
|
||||
[[ $(jq -r '.versionCode' "$play_metadata") == "$android_code" ]]
|
||||
play_track="$(jq -r '.track' "$play_metadata")"
|
||||
normalized_play_track="$(printf '%s' "$play_track" | tr '[:upper:]' '[:lower:]')"
|
||||
[[ $normalized_play_track != production && $normalized_play_track != *:production ]]
|
||||
[[ $(jq -r '.appVersion' "$windows_metadata") == "$version" ]]
|
||||
[[ $(jq -r '.packageVersion' "$windows_metadata") == "$windows_package" ]]
|
||||
grep -F "VniDrop-${version}.dmg" "$appcast" >/dev/null
|
||||
|
||||
mkdir -p "$output_dir"
|
||||
[[ -z $(find "$output_dir" -mindepth 1 -maxdepth 1 -print -quit) ]] || {
|
||||
printf 'Release output directory must be empty: %s\n' "$output_dir" >&2
|
||||
exit 1
|
||||
}
|
||||
cp "$deb" "$rpm" "$dmg" "$appcast" "$play_apk" "$output_dir/"
|
||||
|
||||
payloads=(
|
||||
"$output_dir/$(basename "$deb")"
|
||||
"$output_dir/$(basename "$rpm")"
|
||||
"$output_dir/$(basename "$dmg")"
|
||||
"$output_dir/$(basename "$appcast")"
|
||||
"$output_dir/$(basename "$play_apk")"
|
||||
)
|
||||
files_json="$(
|
||||
for file in "${payloads[@]}"; do
|
||||
jq -n \
|
||||
--arg name "$(basename "$file")" \
|
||||
--arg sha256 "$(sha256sum "$file" | awk '{print $1}')" \
|
||||
--argjson bytes "$(file_size "$file")" \
|
||||
'{name: $name, sha256: $sha256, bytes: $bytes}'
|
||||
done | jq -s .
|
||||
)"
|
||||
|
||||
jq -n \
|
||||
--arg productVersion "$version" \
|
||||
--arg releaseChannel "$("$resolver" channel)" \
|
||||
--arg tag "$source_tag" \
|
||||
--arg commit "$source_commit" \
|
||||
--arg androidVersionCode "$android_code" \
|
||||
--arg appleBuildNumber "$apple_build" \
|
||||
--arg windowsPackageVersion "$windows_package" \
|
||||
--arg windowsMsixUpload "$(basename "$msixupload")" \
|
||||
--arg windowsMsixUploadSha256 "$(sha256sum "$msixupload" | awk '{print $1}')" \
|
||||
--arg playTrack "$play_track" \
|
||||
--arg playBundleSha256 "$(jq -r '.bundleSha256' "$play_metadata")" \
|
||||
--arg playCertificateSha256 "$(jq -r '.appSigningCertificateSha256' "$play_metadata")" \
|
||||
--argjson files "$files_json" \
|
||||
'{
|
||||
productVersion: $productVersion,
|
||||
releaseChannel: $releaseChannel,
|
||||
tag: $tag,
|
||||
sourceCommit: $commit,
|
||||
platformVersions: {
|
||||
androidVersionCode: ($androidVersionCode | tonumber),
|
||||
appleBuildNumber: $appleBuildNumber,
|
||||
windowsPackageVersion: $windowsPackageVersion
|
||||
},
|
||||
play: {
|
||||
track: $playTrack,
|
||||
status: "draft",
|
||||
bundleSha256: $playBundleSha256,
|
||||
appSigningCertificateSha256: $playCertificateSha256
|
||||
},
|
||||
windowsStore: {
|
||||
publicReleaseAsset: false,
|
||||
msixUpload: $windowsMsixUpload,
|
||||
sha256: $windowsMsixUploadSha256
|
||||
},
|
||||
files: $files
|
||||
}' > "$output_dir/release-manifest.json"
|
||||
|
||||
(
|
||||
cd "$output_dir"
|
||||
sha256sum \
|
||||
"$(basename "$deb")" \
|
||||
"$(basename "$rpm")" \
|
||||
"$(basename "$dmg")" \
|
||||
"$(basename "$appcast")" \
|
||||
"$(basename "$play_apk")" \
|
||||
release-manifest.json \
|
||||
> SHA256SUMS
|
||||
)
|
||||
|
||||
printf 'Assembled public release assets in %s\n' "$output_dir"
|
||||
printf 'Windows Store submission retained as workflow artifact: %s\n' \
|
||||
"$(basename "$msixupload")"
|
||||
102
packaging/release/test-assemble-release.sh
Executable file
102
packaging/release/test-assemble-release.sh
Executable file
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_root="$(cd "$script_dir/../.." && pwd)"
|
||||
fixture_root="$(mktemp -d "${TMPDIR:-/tmp}/vnidrop-release-test.XXXXXX")"
|
||||
trap 'rm -rf "$fixture_root"' EXIT
|
||||
|
||||
input_dir="$fixture_root/input"
|
||||
output_dir="$fixture_root/output"
|
||||
mkdir -p \
|
||||
"$input_dir/deb" \
|
||||
"$input_dir/rpm" \
|
||||
"$input_dir/macos" \
|
||||
"$input_dir/play" \
|
||||
"$input_dir/windows"
|
||||
|
||||
version="$("$repo_root/packaging/version/resolve-version.sh" product)"
|
||||
android_code="$("$repo_root/packaging/version/resolve-version.sh" android-code)"
|
||||
windows_package="$("$repo_root/packaging/version/resolve-version.sh" windows-package)"
|
||||
|
||||
printf 'deb\n' > "$input_dir/deb/vnidrop_${version}-1_amd64.deb"
|
||||
printf 'rpm\n' > "$input_dir/rpm/vnidrop-${version}-1.x86_64.rpm"
|
||||
printf 'dmg\n' > "$input_dir/macos/VniDrop-${version}.dmg"
|
||||
printf '<url>VniDrop-%s.dmg</url>\n' "$version" > "$input_dir/macos/appcast.xml"
|
||||
printf 'apk\n' > "$input_dir/play/VniDrop-${version}-${android_code}-play-universal.apk"
|
||||
printf 'msix\n' > "$input_dir/windows/VniDrop_${version}_x64.msix"
|
||||
printf 'msixupload\n' > "$input_dir/windows/VniDrop_${version}_x64.msixupload"
|
||||
|
||||
jq -n \
|
||||
--arg releaseName "$version" \
|
||||
--argjson versionCode "$android_code" \
|
||||
'{
|
||||
releaseStatus: "draft",
|
||||
releaseName: $releaseName,
|
||||
versionCode: $versionCode,
|
||||
track: "closed-beta",
|
||||
bundleSha256: "bundle-sha",
|
||||
appSigningCertificateSha256: "certificate-sha"
|
||||
}' > "$input_dir/play/play-release.json"
|
||||
|
||||
jq -n \
|
||||
--arg appVersion "$version" \
|
||||
--arg packageVersion "$windows_package" \
|
||||
'{appVersion: $appVersion, packageVersion: $packageVersion}' \
|
||||
> "$input_dir/windows/VniDrop_${version}_x64.build-info.json"
|
||||
|
||||
(
|
||||
cd "$input_dir/deb"
|
||||
sha256sum "vnidrop_${version}-1_amd64.deb" \
|
||||
> "vnidrop_${version}-1_amd64.deb.sha256"
|
||||
)
|
||||
(
|
||||
cd "$input_dir/rpm"
|
||||
sha256sum "vnidrop-${version}-1.x86_64.rpm" \
|
||||
> "vnidrop-${version}-1.x86_64.rpm.sha256"
|
||||
)
|
||||
(
|
||||
cd "$input_dir/play"
|
||||
sha256sum \
|
||||
"VniDrop-${version}-${android_code}-play-universal.apk" \
|
||||
play-release.json \
|
||||
> SHA256SUMS
|
||||
)
|
||||
(
|
||||
cd "$input_dir/windows"
|
||||
sha256sum \
|
||||
"VniDrop_${version}_x64.msix" \
|
||||
"VniDrop_${version}_x64.msixupload" \
|
||||
"VniDrop_${version}_x64.build-info.json" \
|
||||
> SHA256SUMS
|
||||
)
|
||||
|
||||
GITHUB_REF_NAME="v$version" \
|
||||
GITHUB_SHA=fixture-commit \
|
||||
VNIDROP_RELEASE_INPUT_DIR="$input_dir" \
|
||||
VNIDROP_RELEASE_OUTPUT_DIR="$output_dir" \
|
||||
"$script_dir/assemble-release.sh" >/dev/null
|
||||
|
||||
expected_public_files=(
|
||||
"SHA256SUMS"
|
||||
"VniDrop-${version}-${android_code}-play-universal.apk"
|
||||
"VniDrop-${version}.dmg"
|
||||
"appcast.xml"
|
||||
"release-manifest.json"
|
||||
"vnidrop-${version}-1.x86_64.rpm"
|
||||
"vnidrop_${version}-1_amd64.deb"
|
||||
)
|
||||
actual_public_files=()
|
||||
while IFS= read -r file; do
|
||||
actual_public_files+=("$(basename "$file")")
|
||||
done < <(find "$output_dir" -maxdepth 1 -type f -print | sort)
|
||||
[[ ${actual_public_files[*]} == "${expected_public_files[*]}" ]]
|
||||
|
||||
[[ $(jq -r '.productVersion' "$output_dir/release-manifest.json") == "$version" ]]
|
||||
[[ $(jq -r '.play.status' "$output_dir/release-manifest.json") == draft ]]
|
||||
[[ $(jq -r '.windowsStore.publicReleaseAsset' "$output_dir/release-manifest.json") == false ]]
|
||||
(
|
||||
cd "$output_dir"
|
||||
sha256sum --check SHA256SUMS >/dev/null
|
||||
)
|
||||
Reference in New Issue
Block a user