#!/usr/bin/env bash set -euo pipefail root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" version="${1:?usage: publish-release.sh VERSION RELEASE_DIRECTORY}" release_dir="${2:?usage: publish-release.sh VERSION RELEASE_DIRECTORY}" api="${RENDEZVOUS_GITEA_API:-https://git.finalfactory.de/api/v1}" registry="${RENDEZVOUS_CONTAINER_REGISTRY:-git.finalfactory.de}" image="$registry/heikyu/rendezvous:$version" token="${RENDEZVOUS_RELEASE_TOKEN:?RENDEZVOUS_RELEASE_TOKEN is required}" username="${RENDEZVOUS_RELEASE_USERNAME:?RENDEZVOUS_RELEASE_USERNAME is required}" release_builder="${RENDEZVOUS_RELEASE_BUILDER:?RENDEZVOUS_RELEASE_BUILDER is required}" docker_config="$(mktemp -d)" curl_config="$(mktemp)" release_request="" release_response="" cleanup() { [[ -z "$release_request" ]] || rm -f "$release_request" [[ -z "$release_response" ]] || rm -f "$release_response" rm -f "$curl_config" rm -rf "$docker_config" } trap cleanup EXIT chmod 0700 "$docker_config" chmod 0600 "$curl_config" printf 'header = "Authorization: token %s"\n' "$token" >"$curl_config" export DOCKER_CONFIG="$docker_config" for command in cosign curl docker dotnet jq; do command -v "$command" >/dev/null || { echo "Required publication command is unavailable: $command" >&2 exit 1 } done run_release_builder() { docker run --rm \ --user "$(id -u):$(id -g)" \ --volume "$root:/source:ro" \ --volume "$release_dir:$release_dir" \ --workdir /source \ "$release_builder" "$@" } "$root/scripts/check-release-tag.sh" "v$version" "$root/scripts/verify-release.sh" "$version" "$release_dir" publish-ready require_absent() { local description="$1" local url="$2" local status status="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' \ --config "$curl_config" "$url")" if [[ "$status" != 404 ]]; then echo "$description must not exist before publication (HTTP $status)." >&2 exit 1 fi } # Gitea package versions are immutable. Require all destinations to be empty # before the first write so a tag can never become a silent partial rerun. require_absent "Client package $version" \ "$api/packages/HeiKyu/nuget/FinalFactory.Rendezvous.Client/$version" require_absent "Contracts package $version" \ "$api/packages/HeiKyu/nuget/FinalFactory.Rendezvous.Contracts/$version" require_absent "Container $version" \ "$api/packages/HeiKyu/container/rendezvous/$version" require_absent "Release v$version" \ "$api/repos/HeiKyu/Rendezvous/releases/tags/v$version" feed="https://git.finalfactory.de/api/packages/HeiKyu/nuget/index.json" for package in \ "$release_dir/FinalFactory.Rendezvous.Contracts.$version.nupkg" \ "$release_dir/FinalFactory.Rendezvous.Client.$version.nupkg"; do dotnet nuget push "$package" \ --source "$feed" \ --api-key "$token" \ --timeout 300 done printf '%s' "$token" | docker login "$registry" --username "$username" --password-stdin expected_image_id="$(jq -er '.containerImageId' "$release_dir/release-provenance.json")" current_image_id="$(docker image inspect --format '{{.Id}}' "$image")" if [[ "$current_image_id" != "$expected_image_id" ]]; then echo "Local release tag changed after staging ($expected_image_id -> $current_image_id)." >&2 exit 1 fi docker push "$image" digest_ref="$(docker inspect --format '{{index .RepoDigests 0}}' "$image")" if [[ ! "$digest_ref" =~ ^git\.finalfactory\.de/heikyu/rendezvous@sha256:[0-9a-f]{64}$ ]]; then echo "Registry did not return an immutable Rendezvous image digest: $digest_ref" >&2 exit 1 fi run_release_builder python3 eng/release_artifacts.py record-container-digest \ --release-dir "$release_dir" \ --digest "$digest_ref" cosign public-key --key env://COSIGN_PRIVATE_KEY >"$release_dir/cosign.pub" run_release_builder ./scripts/finalize-signing-ready-release.sh \ "$version" "$release_dir" cosign sign --yes --key env://COSIGN_PRIVATE_KEY "$digest_ref" cosign attest --yes \ --key env://COSIGN_PRIVATE_KEY \ --type https://finalfactory.de/rendezvous/release-provenance/v1 \ --predicate "$release_dir/release-provenance.json" \ "$digest_ref" cosign sign-blob --yes \ --key env://COSIGN_PRIVATE_KEY \ --bundle "$release_dir/checksums.sha256.bundle" \ "$release_dir/checksums.sha256" "$root/scripts/verify-release.sh" "$version" "$release_dir" published cosign verify --key "$release_dir/cosign.pub" "$digest_ref" >/dev/null cosign verify-attestation \ --key "$release_dir/cosign.pub" \ --type https://finalfactory.de/rendezvous/release-provenance/v1 \ "$digest_ref" >/dev/null cosign verify-blob \ --key "$release_dir/cosign.pub" \ --bundle "$release_dir/checksums.sha256.bundle" \ "$release_dir/checksums.sha256" >/dev/null release_request="$(mktemp)" release_response="$(mktemp)" prerelease=false if [[ "$version" == *-* ]]; then prerelease=true fi jq -n \ --arg tag "v$version" \ --arg commit "${GITHUB_SHA:?GITHUB_SHA is required}" \ --arg digest "$digest_ref" \ --argjson prerelease "$prerelease" \ --rawfile changelog "$release_dir/CHANGELOG.md" \ '{tag_name:$tag,target_commitish:$commit,name:("Rendezvous " + $tag),body:($changelog + "\n\n## Immutable container\n\n`" + $digest + "`\n"),draft:false,prerelease:$prerelease}' \ >"$release_request" curl --fail --silent --show-error \ --request POST \ --config "$curl_config" \ --header 'Content-Type: application/json' \ --data-binary "@$release_request" \ "$api/repos/HeiKyu/Rendezvous/releases" >"$release_response" release_id="$(jq -er '.id' "$release_response")" for artifact in "$release_dir"/*; do curl --fail --silent --show-error \ --request POST \ --config "$curl_config" \ --form "attachment=@$artifact" \ "$api/repos/HeiKyu/Rendezvous/releases/$release_id/assets?name=$(basename "$artifact")" \ >/dev/null done echo "Published immutable release v$version with container $digest_ref"