diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 93b9a21..e172ced 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -20,6 +20,39 @@ jobs: with: fetch-depth: 0 + - name: Resolve host bind sources for sibling containers + shell: bash + run: | + set -euo pipefail + # This job's steps run inside the runner container while every + # `docker run` starts a sibling container on the host daemon, so bind + # sources must be host paths. Resolve them once from the runner's own + # mounts and reuse them in every later step. + if ! mounts="$(docker inspect "$HOSTNAME" 2>/dev/null | jq -c '.[0].Mounts')"; then + echo "Runner is not containerized; using workspace and temp paths as host paths." + echo "RENDEZVOUS_WORKSPACE_SOURCE=$GITHUB_WORKSPACE" >>"$GITHUB_ENV" + echo "RENDEZVOUS_TEMP_SOURCE=$RUNNER_TEMP" >>"$GITHUB_ENV" + exit 0 + fi + resolve_host_path() { + jq -er --arg path "$1" ' + [ .[] + | .Destination as $destination + | select($path == $destination + or ($path | startswith($destination + "/"))) ] + | if length == 0 then + error("No runner mount exposes \($path) on the Docker host.") + else + sort_by(.Destination | length) | last + end + | (.Destination | length) as $prefix + | .Source + $path[$prefix:]' <<<"$mounts" + } + workspace_source="$(resolve_host_path "$GITHUB_WORKSPACE")" + temp_source="$(resolve_host_path "$RUNNER_TEMP")" + echo "RENDEZVOUS_WORKSPACE_SOURCE=$workspace_source" >>"$GITHUB_ENV" + echo "RENDEZVOUS_TEMP_SOURCE=$temp_source" >>"$GITHUB_ENV" + - name: Install pinned .NET SDKs uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: @@ -36,14 +69,19 @@ jobs: - name: Validate tag and produce reproducible artifacts shell: bash + env: + RENDEZVOUS_RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} run: | set -euo pipefail version="${GITHUB_REF_NAME#v}" ./scripts/check-release-tag.sh "$GITHUB_REF_NAME" previous_tag="$(git tag --merged HEAD^ --list 'v*.*.*' --sort=-version:refname | sed -n '1p')" + # check-release-tag.sh already required this tag to point at HEAD, so + # it must be excluded before asking whether any earlier release exists. + unrelated_tag="$(git tag --list 'v*.*.*' | grep -vFx "$GITHUB_REF_NAME" | sed -n '1p' || true)" if [[ -n "$previous_tag" ]]; then ./scripts/check-compatibility.sh "$previous_tag" - elif [[ -n "$(git tag --list 'v*.*.*' | sed -n '1p')" ]]; then + elif [[ -n "$unrelated_tag" ]]; then echo "No prior release tag is an ancestor of $GITHUB_REF_NAME." >&2 exit 1 else @@ -61,14 +99,15 @@ jobs: --user "$(id -u):$(id -g)" \ --env HOME="${RUNNER_TEMP}/release-home" \ --env NUGET_PACKAGES="${RUNNER_TEMP}/nuget" \ - --volume "$GITHUB_WORKSPACE:/source" \ - --volume "${RUNNER_TEMP}:${RUNNER_TEMP}" \ + --volume "$RENDEZVOUS_WORKSPACE_SOURCE:/source" \ + --volume "$RENDEZVOUS_TEMP_SOURCE:${RUNNER_TEMP}" \ --workdir /source \ "$release_builder" \ ./scripts/build-release.sh "$version" "${RUNNER_TEMP}/release/$version" ./scripts/verify-real-consumers.sh "$version" "${RUNNER_TEMP}/release/$version" echo "RENDEZVOUS_VERSION=$version" >>"$GITHUB_ENV" echo "RENDEZVOUS_RELEASE_DIR=${RUNNER_TEMP}/release/$version" >>"$GITHUB_ENV" + echo "RENDEZVOUS_RELEASE_DIR_SOURCE=${RENDEZVOUS_TEMP_SOURCE}/release/$version" >>"$GITHUB_ENV" echo "RENDEZVOUS_RELEASE_BUILDER=$release_builder" >>"$GITHUB_ENV" - name: Build exact container candidate @@ -97,8 +136,8 @@ jobs: buildkit_version="$(docker buildx inspect --bootstrap | sed -n 's/.*BuildKit version: *//p' | sed -n '1p')" docker run --rm \ --user "$(id -u):$(id -g)" \ - --volume "$GITHUB_WORKSPACE:/source" \ - --volume "${RUNNER_TEMP}:${RUNNER_TEMP}" \ + --volume "$RENDEZVOUS_WORKSPACE_SOURCE:/source" \ + --volume "$RENDEZVOUS_TEMP_SOURCE:${RUNNER_TEMP}" \ --workdir /source \ "$RENDEZVOUS_RELEASE_BUILDER" \ python3 eng/release_artifacts.py record-container-build \ @@ -131,9 +170,7 @@ jobs: port_suffix="$(( ${GITHUB_RUN_ID:-$$} % 10000 ))" export RENDEZVOUS_HTTP_HOST_PORT="$(( 20000 + port_suffix ))" export RENDEZVOUS_UDP_HOST_PORT="$(( 40000 + port_suffix ))" - runner_workspace_source="$(docker inspect "$HOSTNAME" | jq -er \ - --arg destination "$GITHUB_WORKSPACE" \ - '.[0].Mounts[] | select(.Destination == $destination) | .Source')" + runner_workspace_source="${RENDEZVOUS_WORKSPACE_SOURCE:?host workspace bind source was not resolved}" export RENDEZVOUS_CONFIG_SOURCE="$runner_workspace_source/deploy/compose/appsettings.Production.json" export RENDEZVOUS_SECRET_SOURCE="$runner_workspace_source/deploy/compose/secrets/signing-key" export RENDEZVOUS_IMAGE="git.finalfactory.de/heikyu/rendezvous:${RENDEZVOUS_VERSION}" @@ -186,8 +223,8 @@ jobs: source_date_epoch="$(git show -s --format=%ct HEAD)" docker run --rm \ --user "$(id -u):$(id -g)" \ - --volume "$GITHUB_WORKSPACE:/source" \ - --volume "${RUNNER_TEMP}:${RUNNER_TEMP}" \ + --volume "$RENDEZVOUS_WORKSPACE_SOURCE:/source" \ + --volume "$RENDEZVOUS_TEMP_SOURCE:${RUNNER_TEMP}" \ --workdir /source \ "$RENDEZVOUS_RELEASE_BUILDER" \ bash -c 'python3 eng/release_artifacts.py normalize-container-sbom \ @@ -218,4 +255,6 @@ jobs: RENDEZVOUS_RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} + RENDEZVOUS_WORKSPACE_SOURCE: ${{ env.RENDEZVOUS_WORKSPACE_SOURCE }} + RENDEZVOUS_RELEASE_DIR_SOURCE: ${{ env.RENDEZVOUS_RELEASE_DIR_SOURCE }} run: ./scripts/publish-release.sh "$RENDEZVOUS_VERSION" "$RENDEZVOUS_RELEASE_DIR" diff --git a/scripts/publish-release.sh b/scripts/publish-release.sh index ef1f204..25367ef 100755 --- a/scripts/publish-release.sh +++ b/scripts/publish-release.sh @@ -10,6 +10,12 @@ 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="" @@ -33,11 +39,21 @@ for command in cosign curl docker dotnet jq; do } 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 "$root:/source:ro" \ - --volume "$release_dir:$release_dir" \ + --volume "$workspace_source:/source:ro" \ + --volume "$release_dir_source:$release_dir" \ --workdir /source \ "$release_builder" "$@" } diff --git a/scripts/verify-real-consumers.sh b/scripts/verify-real-consumers.sh index 73a9b60..a740922 100755 --- a/scripts/verify-real-consumers.sh +++ b/scripts/verify-real-consumers.sh @@ -5,6 +5,11 @@ 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" +# The pinned consumers live in private repositories, so release automation must +# authenticate. Keep the token in the environment git reads config from: it must +# never reach argv, the remote URL, or on-disk repository configuration. +token="${RENDEZVOUS_CONSUMER_TOKEN:-${RENDEZVOUS_RELEASE_TOKEN:-}}" +export GIT_TERMINAL_PROMPT=0 work="$(mktemp -d "${TMPDIR:-/tmp}/rendezvous-consumers.XXXXXX")" cleanup() { rm -rf "$work" @@ -18,6 +23,20 @@ for command in dotnet git jq python3; do } done +fetch_consumer_revision() { + local checkout="$1" + local repository="$2" + local revision="$3" + if [[ -z "$token" ]]; then + git -C "$checkout" fetch --quiet --depth 1 origin "$revision" + return + fi + GIT_CONFIG_COUNT=1 \ + GIT_CONFIG_KEY_0="http.${repository}.extraHeader" \ + GIT_CONFIG_VALUE_0="Authorization: token ${token}" \ + git -C "$checkout" fetch --quiet --depth 1 origin "$revision" +} + python3 "$root/eng/release_artifacts.py" consumer-config \ --local-source "$release_dir" \ --output "$work/NuGet.config" @@ -31,7 +50,7 @@ for ((index = 0; index < count; index++)); do 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" + fetch_consumer_revision "$checkout" "$repository" "$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