254 lines
8.7 KiB
Bash
Executable File
254 lines
8.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
version="${1:-}"
|
|
output="${2:-}"
|
|
|
|
property() {
|
|
sed -n "s:.*<$1>\(.*\)</$1>.*:\1:p" "$root/eng/Versions.props"
|
|
}
|
|
|
|
if [[ -z "$version" ]]; then
|
|
version="$(property RendezvousVersion)"
|
|
fi
|
|
semver='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-([0-9A-Za-z-]+\.)*[0-9A-Za-z-]+)?(\+([0-9A-Za-z-]+\.)*[0-9A-Za-z-]+)?$'
|
|
if [[ ! "$version" =~ $semver ]]; then
|
|
echo "Release version is not valid SemVer: $version" >&2
|
|
exit 1
|
|
fi
|
|
prerelease="${version%%+*}"
|
|
if [[ "$prerelease" == *-* ]]; then
|
|
prerelease="${prerelease#*-}"
|
|
IFS='.' read -r -a prerelease_identifiers <<<"$prerelease"
|
|
for identifier in "${prerelease_identifiers[@]}"; do
|
|
if [[ "$identifier" =~ ^[0-9]+$ && ! "$identifier" =~ ^(0|[1-9][0-9]*)$ ]]; then
|
|
echo "Numeric prerelease identifiers must not contain leading zeroes: $version" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
fi
|
|
if [[ "$version" != "$(property RendezvousVersion)" ]]; then
|
|
echo "Requested version $version differs from eng/Versions.props." >&2
|
|
exit 1
|
|
fi
|
|
|
|
for command in dotnet git python3 tar gzip sha256sum cmp jq; do
|
|
command -v "$command" >/dev/null || {
|
|
echo "Required release command is unavailable: $command" >&2
|
|
exit 1
|
|
}
|
|
done
|
|
|
|
commit="$(git -C "$root" rev-parse HEAD)"
|
|
source_date_epoch="$(git -C "$root" show -s --format=%ct "$commit")"
|
|
tree_state=clean
|
|
if [[ -n "$(git -C "$root" status --porcelain --untracked-files=normal)" ]]; then
|
|
tree_state=dirty
|
|
fi
|
|
allow_dirty=()
|
|
if [[ "$tree_state" != clean ]]; then
|
|
if [[ "${RENDEZVOUS_RELEASE_ALLOW_DIRTY:-0}" != 1 ]]; then
|
|
echo "A formal release must be built from a clean Git tree." >&2
|
|
exit 1
|
|
fi
|
|
allow_dirty=(--allow-dirty)
|
|
fi
|
|
|
|
if [[ -z "$output" ]]; then
|
|
output="$root/artifacts/release/$version"
|
|
fi
|
|
if [[ -e "$output" ]]; then
|
|
echo "Release output already exists; refusing to overwrite: $output" >&2
|
|
exit 1
|
|
fi
|
|
mkdir -p "$(dirname "$output")"
|
|
output="$(cd "$(dirname "$output")" && pwd)/$(basename "$output")"
|
|
|
|
work="$(mktemp -d "${TMPDIR:-/tmp}/rendezvous-release.XXXXXX")"
|
|
cleanup() {
|
|
rm -rf "$work"
|
|
}
|
|
trap cleanup EXIT
|
|
mkdir -p "$work/pack-1" "$work/pack-2" "$work/publish-1" "$work/publish-2" "$output"
|
|
|
|
create_server_archive() {
|
|
local publish_directory="$1"
|
|
local destination="$2"
|
|
tar --sort=name \
|
|
--mtime="@$source_date_epoch" \
|
|
--owner=0 --group=0 --numeric-owner \
|
|
-C "$publish_directory" -cf - . \
|
|
| gzip -n >"$destination"
|
|
}
|
|
|
|
common=(
|
|
-p:ContinuousIntegrationBuild=true
|
|
-p:PackageVersion="$version"
|
|
-p:RepositoryCommit="$commit"
|
|
-p:SourceRevisionId="$commit"
|
|
)
|
|
|
|
cd "$root"
|
|
dotnet restore Rendezvous.slnx --locked-mode
|
|
python3 eng/release_artifacts.py policy --root "$root" >"$work/dependency-policy.json"
|
|
dotnet package list \
|
|
--project Rendezvous.slnx \
|
|
--vulnerable \
|
|
--include-transitive \
|
|
--no-restore \
|
|
--format json >"$work/nuget-vulnerabilities.json"
|
|
python3 eng/release_artifacts.py audit --input "$work/nuget-vulnerabilities.json"
|
|
dotnet format Rendezvous.slnx --verify-no-changes --no-restore
|
|
api_before="$(sha256sum docs/api/*.json)"
|
|
dotnet build Rendezvous.slnx --configuration Release --no-restore "${common[@]}"
|
|
cp src/FinalFactory.Rendezvous.Server/bin/Release/net10.0/FinalFactory.Rendezvous.Server.dll \
|
|
"$work/FinalFactory.Rendezvous.Server.first.dll"
|
|
cp src/FinalFactory.Rendezvous.Server/bin/Release/net10.0/FinalFactory.Rendezvous.Server.pdb \
|
|
"$work/FinalFactory.Rendezvous.Server.first.pdb"
|
|
dotnet publish src/FinalFactory.Rendezvous.Server/FinalFactory.Rendezvous.Server.csproj \
|
|
--configuration Release \
|
|
--no-build \
|
|
--no-restore \
|
|
--output "$work/publish-1" \
|
|
-p:UseAppHost=false \
|
|
-p:OpenApiGenerateDocuments=false \
|
|
"${common[@]}"
|
|
create_server_archive "$work/publish-1" "$work/FinalFactory.Rendezvous.Server.first.tar.gz"
|
|
if [[ "$tree_state" == clean ]]; then
|
|
git diff --exit-code -- docs/api
|
|
elif [[ "$api_before" != "$(sha256sum docs/api/*.json)" ]]; then
|
|
echo "Generated OpenAPI changed during the release build." >&2
|
|
exit 1
|
|
fi
|
|
dotnet test Rendezvous.slnx --configuration Release --no-build
|
|
|
|
for project in Client Contracts; do
|
|
dotnet pack "src/FinalFactory.Rendezvous.$project/FinalFactory.Rendezvous.$project.csproj" \
|
|
--configuration Release --no-build --output "$work/pack-1" "${common[@]}"
|
|
done
|
|
for package in "$work/pack-1"/*; do
|
|
python3 eng/release_artifacts.py normalize-package \
|
|
--package "$package" \
|
|
--source-date-epoch "$source_date_epoch" \
|
|
--version "$version"
|
|
done
|
|
|
|
# Rebuild from the locked graph and prove package byte reproducibility.
|
|
dotnet clean Rendezvous.slnx --configuration Release >/dev/null
|
|
dotnet restore Rendezvous.slnx --locked-mode
|
|
dotnet build Rendezvous.slnx --configuration Release --no-restore "${common[@]}"
|
|
for project in Client Contracts; do
|
|
python3 eng/release_artifacts.py source-link \
|
|
--file "src/FinalFactory.Rendezvous.$project/obj/Release/netstandard2.1/FinalFactory.Rendezvous.$project.sourcelink.json" \
|
|
--commit "$commit"
|
|
done
|
|
cmp --silent \
|
|
"$work/FinalFactory.Rendezvous.Server.first.dll" \
|
|
src/FinalFactory.Rendezvous.Server/bin/Release/net10.0/FinalFactory.Rendezvous.Server.dll || {
|
|
echo "Server assembly is not byte reproducible." >&2
|
|
exit 1
|
|
}
|
|
cmp --silent \
|
|
"$work/FinalFactory.Rendezvous.Server.first.pdb" \
|
|
src/FinalFactory.Rendezvous.Server/bin/Release/net10.0/FinalFactory.Rendezvous.Server.pdb || {
|
|
echo "Server portable PDB is not byte reproducible." >&2
|
|
exit 1
|
|
}
|
|
for project in Client Contracts; do
|
|
dotnet pack "src/FinalFactory.Rendezvous.$project/FinalFactory.Rendezvous.$project.csproj" \
|
|
--configuration Release --no-build --output "$work/pack-2" "${common[@]}"
|
|
done
|
|
for package in "$work/pack-2"/*; do
|
|
python3 eng/release_artifacts.py normalize-package \
|
|
--package "$package" \
|
|
--source-date-epoch "$source_date_epoch" \
|
|
--version "$version"
|
|
done
|
|
for package in "$work/pack-1"/*; do
|
|
cmp --silent "$package" "$work/pack-2/$(basename "$package")" || {
|
|
echo "Package is not byte reproducible: $(basename "$package")" >&2
|
|
exit 1
|
|
}
|
|
done
|
|
cp "$work/pack-1"/* "$output/"
|
|
|
|
python3 eng/release_artifacts.py consumer-config \
|
|
--local-source "$output" \
|
|
--output "$work/consumer.NuGet.config"
|
|
for consumer in spacegame unscouted; do
|
|
project="$root/tests/consumers/$consumer/$(find "$root/tests/consumers/$consumer" -maxdepth 1 -name '*.csproj' -printf '%f\n')"
|
|
packages="$work/consumer-packages-$consumer"
|
|
dotnet restore "$project" \
|
|
-p:RendezvousPackageVersion="$version" \
|
|
-p:RestoreLockedMode=false \
|
|
--packages "$packages" \
|
|
--configfile "$work/consumer.NuGet.config" \
|
|
--force-evaluate
|
|
dotnet build "$project" \
|
|
--configuration Release --no-restore \
|
|
-p:RendezvousPackageVersion="$version"
|
|
assets="$(dirname "$project")/obj/project.assets.json"
|
|
python3 eng/release_artifacts.py consumer --assets "$assets" --version "$version"
|
|
done
|
|
|
|
dotnet publish src/FinalFactory.Rendezvous.Server/FinalFactory.Rendezvous.Server.csproj \
|
|
--configuration Release \
|
|
--no-build \
|
|
--no-restore \
|
|
--output "$work/publish-2" \
|
|
-p:UseAppHost=false \
|
|
-p:OpenApiGenerateDocuments=false \
|
|
"${common[@]}"
|
|
server_archive="$output/FinalFactory.Rendezvous.Server.$version.linux-x64.tar.gz"
|
|
create_server_archive "$work/publish-2" "$server_archive"
|
|
cmp --silent "$work/FinalFactory.Rendezvous.Server.first.tar.gz" "$server_archive" || {
|
|
echo "Server archive is not byte reproducible." >&2
|
|
exit 1
|
|
}
|
|
|
|
cp CHANGELOG.md "$output/CHANGELOG.md"
|
|
python3 eng/release_artifacts.py sbom \
|
|
--root "$root" \
|
|
--version "$version" \
|
|
--commit "$commit" \
|
|
--source-date-epoch "$source_date_epoch" \
|
|
--server-deps "$work/publish-2/FinalFactory.Rendezvous.Server.deps.json" \
|
|
--output "$output/FinalFactory.Rendezvous.$version.spdx.json"
|
|
|
|
provenance=(
|
|
provenance
|
|
--root "$root"
|
|
--version "$version"
|
|
--commit "$commit"
|
|
--tree-state "$tree_state"
|
|
--source-date-epoch "$source_date_epoch"
|
|
--dotnet-sdk "$(dotnet --version)"
|
|
--build-tool "python=$(python3 --version 2>&1)"
|
|
--build-tool "tar=$(tar --version | sed -n '1p')"
|
|
--build-tool "gzip=$(gzip --version | sed -n '1p')"
|
|
--build-tool "jq=$(jq --version)"
|
|
--output "$output/release-provenance.json"
|
|
)
|
|
while IFS= read -r base; do
|
|
provenance+=(--base-digest "$base")
|
|
done < <(sed -n 's/^FROM \([^ ]*\).*/\1/p' Dockerfile)
|
|
while IFS= read -r base; do
|
|
provenance+=(--builder-base "$base")
|
|
done < <(sed -n 's/^FROM \([^ ]*\).*/\1/p' eng/release-builder.Dockerfile)
|
|
python3 eng/release_artifacts.py "${provenance[@]}"
|
|
|
|
(
|
|
cd "$output"
|
|
find . -maxdepth 1 -type f ! -name checksums.sha256 -printf '%f\n' \
|
|
| LC_ALL=C sort \
|
|
| xargs sha256sum >checksums.sha256
|
|
)
|
|
python3 eng/release_artifacts.py verify \
|
|
--root "$root" \
|
|
--release-dir "$output" \
|
|
--version "$version" \
|
|
"${allow_dirty[@]}"
|
|
|
|
echo "Release artifacts verified at $output"
|