#!/usr/bin/env bash set -euo pipefail root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" version="${1:-}" output="${2:-}" property() { sed -n "s:.*<$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"