feat(release): add reproducible signed artifacts (#19)
This commit is contained in:
Executable
+253
@@ -0,0 +1,253 @@
|
||||
#!/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"
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
base="${1:-origin/main}"
|
||||
|
||||
if ! git -C "$root" cat-file -e "$base^{commit}" 2>/dev/null; then
|
||||
echo "Compatibility base does not exist; this is valid only for the initial version baseline: $base"
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$(git -C "$root" rev-parse "$base")" == "$(git -C "$root" rev-parse HEAD)" ]]; then
|
||||
base="HEAD^"
|
||||
fi
|
||||
if ! git -C "$root" cat-file -e "$base:eng/Versions.props" 2>/dev/null; then
|
||||
echo "Base has no release version manifest; accepting the initial compatibility baseline."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
current_property() {
|
||||
sed -n "s:.*<$1>\(.*\)</$1>.*:\1:p" "$root/eng/Versions.props"
|
||||
}
|
||||
base_property() {
|
||||
git -C "$root" show "$base:eng/Versions.props" \
|
||||
| sed -n "s:.*<$1>\(.*\)</$1>.*:\1:p"
|
||||
}
|
||||
changed() {
|
||||
git -C "$root" diff --name-only "$base"...HEAD -- "$@" | grep -q .
|
||||
}
|
||||
require_increase() {
|
||||
local property="$1"
|
||||
local description="$2"
|
||||
shift 2
|
||||
if changed "$@"; then
|
||||
local before after
|
||||
before="$(base_property "$property")"
|
||||
after="$(current_property "$property")"
|
||||
if [[ ! "$before" =~ ^[0-9]+$ || ! "$after" =~ ^[0-9]+$ || "$after" -le "$before" ]]; then
|
||||
echo "$description changed without increasing $property ($before -> $after)." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
require_increase RendezvousMajorVersion "Published .NET API snapshot" \
|
||||
'tests/FinalFactory.Rendezvous.Tests/TestData/Contracts/v*/client-public-api.txt' \
|
||||
'tests/FinalFactory.Rendezvous.Tests/TestData/Contracts/v*/contracts-public-api.txt'
|
||||
require_increase HttpContractVersion "HTTP/OpenAPI contract evidence" \
|
||||
'docs/api/*.json' \
|
||||
'tests/FinalFactory.Rendezvous.Tests/TestData/Contracts/v*/*.json' \
|
||||
':(exclude)tests/FinalFactory.Rendezvous.Tests/TestData/Contracts/v*/connection-ticket.json'
|
||||
require_increase UdpContractVersion "UDP contract evidence" \
|
||||
'tests/FinalFactory.Rendezvous.Tests/TestData/Contracts/v*/*.hex'
|
||||
require_increase ConnectionTicketFormatVersion "Connection-ticket format evidence" \
|
||||
'tests/FinalFactory.Rendezvous.Tests/TestData/Contracts/v*/connection-ticket.json'
|
||||
|
||||
echo "Compatibility changes are paired with the required version increase."
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
tag="${1:-${GITHUB_REF_NAME:-}}"
|
||||
version="$(sed -n 's:.*<RendezvousVersion>\(.*\)</RendezvousVersion>.*:\1:p' "$root/eng/Versions.props")"
|
||||
|
||||
if [[ "$tag" != "v$version" ]]; then
|
||||
echo "Release tag $tag does not match central version v$version." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -n "$(git -C "$root" status --porcelain --untracked-files=normal)" ]]; then
|
||||
echo "Release tag checkout is not clean." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$(git -C "$root" tag --points-at HEAD --list "$tag")" != "$tag" ]]; then
|
||||
echo "Release tag $tag does not point at the checked-out commit." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -Eq "^## $version - [0-9]{4}-[0-9]{2}-[0-9]{2}$" "$root/CHANGELOG.md"; then
|
||||
echo "CHANGELOG.md must contain a dated heading for $version." >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -Eq "^## $version - Unreleased$" "$root/CHANGELOG.md"; then
|
||||
echo "Release $version is still marked Unreleased." >&2
|
||||
exit 1
|
||||
fi
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
version="${1:?usage: finalize-release-candidate.sh VERSION RELEASE_DIRECTORY}"
|
||||
release_dir="${2:?usage: finalize-release-candidate.sh VERSION RELEASE_DIRECTORY}"
|
||||
container_sbom="$release_dir/FinalFactory.Rendezvous.Container.$version.spdx.json"
|
||||
|
||||
[[ -s "$container_sbom" ]] || {
|
||||
echo "Container SBOM is missing or empty: $container_sbom" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
(
|
||||
cd "$release_dir"
|
||||
find . -maxdepth 1 -type f ! -name checksums.sha256 -printf '%f\n' \
|
||||
| LC_ALL=C sort \
|
||||
| xargs sha256sum >checksums.sha256
|
||||
)
|
||||
python3 "$root/eng/release_artifacts.py" verify \
|
||||
--root "$root" \
|
||||
--release-dir "$release_dir" \
|
||||
--version "$version" \
|
||||
--phase publish-ready
|
||||
|
||||
echo "Finalized publish-ready release candidate $version"
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
version="${1:?usage: finalize-signing-ready-release.sh VERSION RELEASE_DIRECTORY}"
|
||||
release_dir="${2:?usage: finalize-signing-ready-release.sh VERSION RELEASE_DIRECTORY}"
|
||||
|
||||
(
|
||||
cd "$release_dir"
|
||||
find . -maxdepth 1 -type f \
|
||||
! -name checksums.sha256 \
|
||||
! -name checksums.sha256.bundle \
|
||||
-printf '%f\n' \
|
||||
| LC_ALL=C sort \
|
||||
| xargs sha256sum >checksums.sha256
|
||||
)
|
||||
python3 "$root/eng/release_artifacts.py" verify \
|
||||
--root "$root" \
|
||||
--release-dir "$release_dir" \
|
||||
--version "$version" \
|
||||
--phase signing-ready
|
||||
|
||||
echo "Finalized signing-ready release $version"
|
||||
Executable
+153
@@ -0,0 +1,153 @@
|
||||
#!/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"
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
version="${1:?usage: verify-real-consumers.sh VERSION RELEASE_DIRECTORY}"
|
||||
release_dir="${2:?usage: verify-real-consumers.sh VERSION RELEASE_DIRECTORY}"
|
||||
manifest="$root/eng/consumer-revisions.json"
|
||||
work="$(mktemp -d "${TMPDIR:-/tmp}/rendezvous-consumers.XXXXXX")"
|
||||
cleanup() {
|
||||
rm -rf "$work"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
for command in dotnet git jq python3; do
|
||||
command -v "$command" >/dev/null || {
|
||||
echo "Required consumer verification command is unavailable: $command" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
python3 "$root/eng/release_artifacts.py" consumer-config \
|
||||
--local-source "$release_dir" \
|
||||
--output "$work/NuGet.config"
|
||||
|
||||
count="$(jq '.consumers | length' "$manifest")"
|
||||
for ((index = 0; index < count; index++)); do
|
||||
name="$(jq -r ".consumers[$index].name" "$manifest")"
|
||||
repository="$(jq -r ".consumers[$index].repository" "$manifest")"
|
||||
revision="$(jq -r ".consumers[$index].revision" "$manifest")"
|
||||
project_relative="$(jq -r ".consumers[$index].project" "$manifest")"
|
||||
checkout="$work/$name"
|
||||
git -c init.defaultBranch=main init --quiet "$checkout"
|
||||
git -C "$checkout" remote add origin "$repository"
|
||||
git -C "$checkout" fetch --quiet --depth 1 origin "$revision"
|
||||
GIT_LFS_SKIP_SMUDGE=1 git -C "$checkout" checkout --quiet --detach FETCH_HEAD
|
||||
[[ "$(git -C "$checkout" rev-parse HEAD)" == "$revision" ]] || {
|
||||
echo "$name did not resolve the pinned consumer revision." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
project="$checkout/$project_relative"
|
||||
[[ -f "$project" ]] || {
|
||||
echo "$name consumer project does not exist at $project_relative." >&2
|
||||
exit 1
|
||||
}
|
||||
targets="$work/$name.Rendezvous.Consumer.targets"
|
||||
cat >"$targets" <<EOF
|
||||
<Project>
|
||||
<ItemGroup Condition="'\$(MSBuildProjectFullPath)' == '$project'">
|
||||
<PackageReference Include="FinalFactory.Rendezvous.Client" Version="[$version]" />
|
||||
<PackageReference Include="FinalFactory.Rendezvous.Contracts" Version="[$version]" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
EOF
|
||||
packages="$work/packages-$name"
|
||||
dotnet restore "$project" \
|
||||
-p:CustomAfterMicrosoftCommonTargets="$targets" \
|
||||
-p:RestorePackagesWithLockFile=false \
|
||||
-p:RestoreLockedMode=false \
|
||||
--packages "$packages" \
|
||||
--configfile "$work/NuGet.config" \
|
||||
--force-evaluate
|
||||
assets=""
|
||||
while IFS= read -r candidate_assets; do
|
||||
if grep -Fq "FinalFactory.Rendezvous.Client/$version" "$candidate_assets"; then
|
||||
assets="$candidate_assets"
|
||||
break
|
||||
fi
|
||||
done < <(find "$checkout" -path '*/obj/project.assets.json' -type f -print)
|
||||
[[ -n "$assets" ]] || {
|
||||
echo "$name restore did not produce assets for the injected Rendezvous references." >&2
|
||||
exit 1
|
||||
}
|
||||
python3 "$root/eng/release_artifacts.py" consumer \
|
||||
--assets "$assets" \
|
||||
--version "$version"
|
||||
echo "Verified $name at $revision can pin and restore Rendezvous $version."
|
||||
done
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
version="${1:?usage: verify-release.sh VERSION [RELEASE_DIRECTORY] [PHASE]}"
|
||||
release_dir="${2:-$root/artifacts/release/$version}"
|
||||
phase="${3:-build}"
|
||||
|
||||
python3 "$root/eng/release_artifacts.py" verify \
|
||||
--root "$root" \
|
||||
--release-dir "$release_dir" \
|
||||
--version "$version" \
|
||||
--phase "$phase"
|
||||
Reference in New Issue
Block a user