The release workflow could not complete on the self-hosted sibling-container runner. Four defects are corrected together because three of them share the same release.yml plumbing. Exclude the tag under release from the prior-tag probe. check-release-tag.sh already requires the tag to point at HEAD, so the unfiltered listing always matched itself and rejected every first release before the initial baseline branch could run. Resolve host bind sources once per job. Steps run inside the runner container while docker run starts siblings on the host daemon, so GITHUB_WORKSPACE and RUNNER_TEMP are not usable as bind sources. A new step maps both to host paths through the runner's own mounts and exports them; the build, provenance, smoke, finalize, and publish steps reuse them, and publish-release.sh receives them by environment instead of repeating the inspection. Bind destinations stay at the container-internal paths so runner-side reads keep working unchanged. Prove the signing material before the first push. Gitea package versions are immutable, so a missing or undecryptable cosign key must abort ahead of the require_absent gate rather than after packages and the image are published. Authenticate the pinned consumer fetches. Both consumer repositories are private, so anonymous fetches fail; the release token is passed to the verification step and applied as a URL-scoped extra header through git's config environment, keeping it out of argv, remotes, and on-disk config. Anonymous fetch remains the fallback for local runs.
170 lines
6.6 KiB
Bash
Executable File
170 lines
6.6 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 bind sources must be
|
|
# host paths. The workflow resolves them once and exports them; a direct host
|
|
# run keeps the local paths.
|
|
workspace_source="${RENDEZVOUS_WORKSPACE_SOURCE:-$root}"
|
|
release_dir_source="${RENDEZVOUS_RELEASE_DIR_SOURCE:-$release_dir}"
|
|
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:/source:ro" \
|
|
--volume "$release_dir_source:$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"
|