Files
Rendezvous/.gitea/workflows/release.yml
T
KyuubiYoru 562b8308b7
quality-gate / quality (push) Successful in 2m47s
quality-gate / container (push) Successful in 1m51s
immutable-release / release (push) Failing after 5m40s
fix(release): run trivy from a pinned verified binary (#1)
trivy-action checks its own repository out of github.com using the runner
token; on this self-hosted Gitea that token is a Gitea token, GitHub answers
"Bad credentials", and both scan steps die before trivy is installed.

Download the v0.69.3 release archive directly, verify it against a sha256
digest pinned inline, and unpack only the binary into .release-work/bin,
which is gitignored and excluded from the Docker build context. The gate
keeps its exact semantics: --exit-code 1, --severity HIGH,CRITICAL, table
output, unfixed vulnerabilities still in scope. The SPDX step writes the
same filename release_artifacts.py normalize-container-sbom consumes, and
TRIVY_CACHE_DIR keeps the vulnerability DB inside the work directory.
2026-08-22 21:06:21 +02:00

286 lines
13 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 the host bind source 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. The workspace is the only runner mount
# backed by the host, so every path shared with a sibling lives under
# it and a single bind source is resolved once here. Siblings mount
# that source at $GITHUB_WORKSPACE, which keeps every shared path the
# same string on both sides of the boundary.
echo "RENDEZVOUS_WORK_DIR=$GITHUB_WORKSPACE/.release-work" >>"$GITHUB_ENV"
if ! mounts="$(docker inspect "$HOSTNAME" 2>/dev/null | jq -c '.[0].Mounts')"; then
echo "Runner is not containerized; using the workspace path as its own host path."
echo "RENDEZVOUS_WORKSPACE_SOURCE=$GITHUB_WORKSPACE" >>"$GITHUB_ENV"
exit 0
fi
workspace_source="$(jq -er --arg path "$GITHUB_WORKSPACE" '
[ .[]
| .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")"
echo "RENDEZVOUS_WORKSPACE_SOURCE=$workspace_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" .
release_dir="$RENDEZVOUS_WORK_DIR/release/$version"
mkdir -p "$RENDEZVOUS_WORK_DIR/release-home" "$RENDEZVOUS_WORK_DIR/nuget"
docker run --rm \
--user "$(id -u):$(id -g)" \
--env HOME="$RENDEZVOUS_WORK_DIR/release-home" \
--env NUGET_PACKAGES="$RENDEZVOUS_WORK_DIR/nuget" \
--volume "$RENDEZVOUS_WORKSPACE_SOURCE:$GITHUB_WORKSPACE" \
--workdir "$GITHUB_WORKSPACE" \
"$release_builder" \
./scripts/build-release.sh "$version" "$release_dir"
./scripts/verify-real-consumers.sh "$version" "$release_dir"
echo "RENDEZVOUS_VERSION=$version" >>"$GITHUB_ENV"
echo "RENDEZVOUS_RELEASE_DIR=$release_dir" >>"$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}"
# .release-work is excluded from the build context, so the tar written
# by the first build cannot change the context the second one sees.
image_one="$RENDEZVOUS_WORK_DIR/rendezvous-image-1.tar"
image_two="$RENDEZVOUS_WORK_DIR/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:$GITHUB_WORKSPACE" \
--workdir "$GITHUB_WORKSPACE" \
"$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:$GITHUB_WORKSPACE:ro" \
--workdir "$GITHUB_WORKSPACE" \
"$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: Install pinned vulnerability scanner
shell: bash
run: |
set -euo pipefail
# trivy-action checks its own repository out of github.com with the
# runner token, which this Gitea instance cannot mint, so the release
# binary is fetched directly instead. The archive URL and its digest
# are pinned inline: fetching the published checksums file at run time
# would only prove the asset matches whatever the release currently
# serves, which is exactly what pinning has to rule out.
trivy_bin_dir="$RENDEZVOUS_WORK_DIR/bin"
trivy_archive="$RENDEZVOUS_WORK_DIR/trivy_0.69.3_Linux-64bit.tar.gz"
# .release-work is gitignored and excluded from the Docker build
# context, so nothing unpacked here can reach an image layer.
mkdir -p "$trivy_bin_dir" "$RENDEZVOUS_WORK_DIR/trivy-cache"
curl --fail --location --silent --show-error --output "$trivy_archive" \
https://github.com/aquasecurity/trivy/releases/download/v0.69.3/trivy_0.69.3_Linux-64bit.tar.gz
echo "1816b632dfe529869c740c0913e36bd1629cb7688bd5634f4a858c1d57c88b75 $trivy_archive" \
| sha256sum --check --strict -
tar --extract --file "$trivy_archive" --directory "$trivy_bin_dir" trivy
rm -f "$trivy_archive"
chmod +x "$trivy_bin_dir/trivy"
echo "RENDEZVOUS_TRIVY=$trivy_bin_dir/trivy" >>"$GITHUB_ENV"
echo "TRIVY_CACHE_DIR=$RENDEZVOUS_WORK_DIR/trivy-cache" >>"$GITHUB_ENV"
"$trivy_bin_dir/trivy" --version
- name: Scan candidate for high and critical vulnerabilities
shell: bash
run: |
set -euo pipefail
# Unfixed vulnerabilities stay in scope: the flag that would drop them
# is never passed, so the gate keeps trivy's fail-closed default.
"$RENDEZVOUS_TRIVY" image \
--exit-code 1 \
--severity HIGH,CRITICAL \
--format table \
"git.finalfactory.de/heikyu/rendezvous:${RENDEZVOUS_VERSION}"
- name: Generate container SPDX inventory
shell: bash
run: |
set -euo pipefail
"$RENDEZVOUS_TRIVY" image \
--format spdx-json \
--output "$RENDEZVOUS_RELEASE_DIR/FinalFactory.Rendezvous.Container.$RENDEZVOUS_VERSION.spdx.json" \
"git.finalfactory.de/heikyu/rendezvous:${RENDEZVOUS_VERSION}"
- 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:$GITHUB_WORKSPACE" \
--workdir "$GITHUB_WORKSPACE" \
"$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 }}
run: ./scripts/publish-release.sh "$RENDEZVOUS_VERSION" "$RENDEZVOUS_RELEASE_DIR"