The release job stopped at the bind-source gate. RUNNER_TEMP is container-internal on this runner and no runner mount exposes it on the Docker host, so no sibling container could ever share it; only the workspace is host-mounted. Move every path shared between the runner's shell steps and its sibling containers under $GITHUB_WORKSPACE/.release-work: the release directory, the release builder's HOME and NuGet cache, both candidate image tars, and the container SPDX inventory. Resolution now maps the workspace alone to its host path, and each sibling binds that source at $GITHUB_WORKSPACE and works from there instead of /source, so a shared path is the same string on both sides of the boundary. publish-release.sh follows with a single bind and requires the release directory to sit inside the workspace. Ignore .release-work in Git so the tag gate's cleanliness check stays true while artifacts accumulate, and in Docker so artifacts written between the two candidate builds cannot alter the build context the byte-comparison gate depends on. Skip it in the dependency inventory as well, keeping the restored package cache out of the license policy scan.
179 lines
6.9 KiB
Bash
Executable File
179 lines
6.9 KiB
Bash
Executable File
#!/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}"
|
|
# Sibling-container runners execute this script inside a container while
|
|
# `docker run` starts containers on the host daemon, so the bind source must be
|
|
# a host path. The workflow resolves the workspace once and exports it; a direct
|
|
# host run keeps the local path. Only the workspace is bound, so the release
|
|
# directory has to live inside it, and binding it back onto its own path keeps
|
|
# every shared path identical on both sides of the boundary.
|
|
workspace_source="${RENDEZVOUS_WORKSPACE_SOURCE:-$root}"
|
|
[[ -d "$release_dir" ]] || {
|
|
echo "Release directory does not exist: $release_dir" >&2
|
|
exit 1
|
|
}
|
|
release_dir="$(cd "$release_dir" && pwd)"
|
|
if [[ "$release_dir" != "$root"/* ]]; then
|
|
echo "Release directory must live inside the workspace: $release_dir" >&2
|
|
exit 1
|
|
fi
|
|
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
|
|
|
|
# Publication is a one-way gate: Gitea package versions are immutable, so the
|
|
# signing material must be proven usable before anything is pushed. Never print
|
|
# the key or the probe output.
|
|
: "${COSIGN_PRIVATE_KEY:?COSIGN_PRIVATE_KEY is required}"
|
|
: "${COSIGN_PASSWORD:?COSIGN_PASSWORD is required}"
|
|
cosign public-key --key env://COSIGN_PRIVATE_KEY >/dev/null || {
|
|
echo "Configured cosign key could not be loaded; refusing to publish." >&2
|
|
exit 1
|
|
}
|
|
|
|
run_release_builder() {
|
|
docker run --rm \
|
|
--user "$(id -u):$(id -g)" \
|
|
--volume "$workspace_source:$root" \
|
|
--workdir "$root" \
|
|
"$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"
|