Files
Rendezvous/.gitea/workflows/release.yml
T
KyuubiYoru d41fa0c7e9 fix(release): repair tag gate and sibling-container publication (#1)
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.
2026-08-22 19:47:57 +02:00

261 lines
12 KiB
YAML

name: immutable-release
on:
push:
tags:
- "v*.*.*"
concurrency:
group: release-${{ gitea.ref_name }}
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
timeout-minutes: 45
environment: production
steps:
- name: Check out immutable tag
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
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:
dotnet-version: |
8.0.128
10.0.301
- name: Install pinned Buildx and BuildKit
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
with:
version: v0.35.0
install: true
driver-opts: image=moby/buildkit:v0.25.2@sha256:0f63d66f8d2de0bd16438284831a3e9ee6ca7cd57b6eb3ed6e38a7a456590fa7
- 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 "$unrelated_tag" ]]; then
echo "No prior release tag is an ancestor of $GITHUB_REF_NAME." >&2
exit 1
else
./scripts/check-compatibility.sh __initial_release_without_base__
fi
release_builder="rendezvous-release-builder:${GITHUB_SHA}"
docker buildx build \
--platform linux/amd64 \
--file eng/release-builder.Dockerfile \
--target release-builder \
--load \
--tag "$release_builder" .
mkdir -p "${RUNNER_TEMP}/release-home" "${RUNNER_TEMP}/nuget"
docker run --rm \
--user "$(id -u):$(id -g)" \
--env HOME="${RUNNER_TEMP}/release-home" \
--env NUGET_PACKAGES="${RUNNER_TEMP}/nuget" \
--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
shell: bash
run: |
set -euo pipefail
export SOURCE_DATE_EPOCH="$(git show -s --format=%ct HEAD)"
common=(
--no-cache
--pull=false
--provenance=false
--platform linux/amd64
--build-arg SOURCE_DATE_EPOCH="$SOURCE_DATE_EPOCH"
--build-arg SOURCE_REVISION_ID="$GITHUB_SHA"
)
release_tag="git.finalfactory.de/heikyu/rendezvous:${RENDEZVOUS_VERSION}"
image_one="${RUNNER_TEMP}/rendezvous-image-1.tar"
image_two="${RUNNER_TEMP}/rendezvous-image-2.tar"
docker buildx build "${common[@]}" --tag "$release_tag" \
--output "type=docker,dest=$image_one,rewrite-timestamp=true" .
docker buildx build "${common[@]}" --tag "$release_tag" \
--output "type=docker,dest=$image_two,rewrite-timestamp=true" .
cmp --silent "$image_one" "$image_two"
docker load --input "$image_one"
candidate_id="$(docker image inspect --format '{{.Id}}' "$release_tag")"
buildkit_version="$(docker buildx inspect --bootstrap | sed -n 's/.*BuildKit version: *//p' | sed -n '1p')"
docker run --rm \
--user "$(id -u):$(id -g)" \
--volume "$RENDEZVOUS_WORKSPACE_SOURCE:/source" \
--volume "$RENDEZVOUS_TEMP_SOURCE:${RUNNER_TEMP}" \
--workdir /source \
"$RENDEZVOUS_RELEASE_BUILDER" \
python3 eng/release_artifacts.py record-container-build \
--provenance "$RENDEZVOUS_RELEASE_DIR/release-provenance.json" \
--buildx-version "$(docker buildx version)" \
--buildkit-version "$buildkit_version" \
--image-id "$candidate_id"
- name: Stage HTTP registration, browse, and authenticated UDP traversal
shell: bash
run: |
set -euo pipefail
secret="deploy/compose/secrets/signing-key"
cleanup() {
RENDEZVOUS_UID=1654 RENDEZVOUS_GID=1654 RENDEZVOUS_IMAGE="git.finalfactory.de/heikyu/rendezvous:${RENDEZVOUS_VERSION}" \
docker compose -f deploy/compose/compose.yaml down --volumes >/dev/null 2>&1 || true
rm -f "$secret"
}
trap cleanup EXIT
umask 077
install -d -m 0700 deploy/compose/secrets
openssl rand -out "$secret" 32
chmod 0600 "$secret"
publisher_credential="$(RENDEZVOUS_SMOKE_LOCAL_KEY="$secret" \
./scripts/mint-local-publisher-credential.sh)"
export RENDEZVOUS_PUBLISHER_CREDENTIAL="$publisher_credential"
chmod 0444 "$secret"
export RENDEZVOUS_UID=1654
export RENDEZVOUS_GID=1654
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="${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}"
docker compose -f deploy/compose/compose.yaml up --detach --no-build
container_id="$(docker compose -f deploy/compose/compose.yaml ps -q rendezvous)"
test -n "$container_id"
docker run --rm \
--network "container:${container_id}" \
--user "$(id -u):$(id -g)" \
--env HOME=/tmp \
--env RENDEZVOUS_SMOKE_HTTP_URL=http://127.0.0.1:8080/ \
--env RENDEZVOUS_SMOKE_UDP_ENDPOINT=127.0.0.1:9050 \
--env RENDEZVOUS_PUBLISHER_CREDENTIAL \
--volume "$runner_workspace_source:/source:ro" \
--workdir /source \
"$RENDEZVOUS_RELEASE_BUILDER" \
bash -lc '
for attempt in {1..180}; do
curl --fail --silent "${RENDEZVOUS_SMOKE_HTTP_URL%/}/health/ready" >/dev/null 2>&1 && exec ./scripts/smoke-deployment.sh
sleep 1
done
exit 1
' || {
docker compose -f deploy/compose/compose.yaml logs rendezvous
exit 1
}
- name: Scan candidate for high and critical vulnerabilities
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0, post-incident safe SHA
with:
image-ref: git.finalfactory.de/heikyu/rendezvous:${{ env.RENDEZVOUS_VERSION }}
version: v0.69.3
format: table
exit-code: "1"
ignore-unfixed: false
severity: HIGH,CRITICAL
- name: Generate container SPDX inventory
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0, post-incident safe SHA
with:
image-ref: git.finalfactory.de/heikyu/rendezvous:${{ env.RENDEZVOUS_VERSION }}
version: v0.69.3
format: spdx-json
output: ${{ env.RENDEZVOUS_RELEASE_DIR }}/FinalFactory.Rendezvous.Container.${{ env.RENDEZVOUS_VERSION }}.spdx.json
- name: Finalize checksums over the publish-ready candidate
shell: bash
run: |
set -euo pipefail
source_date_epoch="$(git show -s --format=%ct HEAD)"
docker run --rm \
--user "$(id -u):$(id -g)" \
--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 \
--file "$1/FinalFactory.Rendezvous.Container.$2.spdx.json" \
--version "$2" \
--commit "$3" \
--source-date-epoch "$4" \
&& ./scripts/finalize-release-candidate.sh "$2" "$1"' \
_ "$RENDEZVOUS_RELEASE_DIR" "$RENDEZVOUS_VERSION" "$GITHUB_SHA" "$source_date_epoch"
- name: Preserve verified candidate artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: rendezvous-${{ env.RENDEZVOUS_VERSION }}
path: ${{ env.RENDEZVOUS_RELEASE_DIR }}
if-no-files-found: error
retention-days: 30
- name: Install pinned signing client
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
with:
cosign-release: v3.0.6
- name: Publish once, sign, attest, and create release
shell: bash
env:
RENDEZVOUS_RELEASE_USERNAME: ${{ secrets.RELEASE_USERNAME }}
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"