Files
KyuubiYoru e3b687c903
quality-gate / quality (push) Successful in 2m35s
quality-gate / container (push) Successful in 1m38s
immutable-release / release (push) Successful in 6m26s
fix(release): keep publication off GitHub-only actions (#1)
upload-artifact@v4 speaks the v2 artifacts API, which this Gitea answers
only at its v3-era shape, so the sixth release attempt cleared every gate
and then aborted at "Preserve verified candidate artifacts" with the GHES
compatibility error. Move the step to v3.2.1, the commit the upstream v3
tag resolves to, which keeps name, path, if-no-files-found and
retention-days unchanged. The step also becomes continue-on-error: it is a
pre-publish debugging backstop, and publish-release.sh attaches the same
directory as Gitea release assets, so losing it must never cost a release.

Replace cosign-installer with the direct fetch the scanner already uses.
The action issues no API call on this path, but it is a composite action
resting on envsubst and a resolved runner.arch, neither of which this
runner has exercised. Asked for the version it bootstraps, it downloads
exactly cosign-linux-amd64 from the v3.0.6 release, compares it against
c956e5df..., and exits; that digest matches the release checksums file, so
fetching the asset directly verifies identically with nothing unproven
left before the one-way publication gate. The binary joins the PATH that
publish-release.sh already resolves dotnet through.
2026-08-22 22:10:00 +02:00

312 lines
15 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
# upload-artifact@v4 speaks the v2 artifacts API, which this Gitea only
# answers at its v3-era shape, so v4 aborts before uploading anything.
# Keeping the candidate is a pre-publish debugging backstop, not a
# release gate: publish-release.sh attaches the same files as Gitea
# release assets, so a failure here must never block a release.
continue-on-error: true
uses: actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3.2.1
with:
name: rendezvous-${{ env.RENDEZVOUS_VERSION }}
path: ${{ env.RENDEZVOUS_RELEASE_DIR }}
if-no-files-found: error
retention-days: 30
- name: Install pinned signing client
shell: bash
run: |
set -euo pipefail
# cosign-installer issues no API call on the pinned path, but it is a
# composite action resting on `envsubst` and a resolved runner.arch,
# neither of which this runner has ever exercised. Asked for the same
# version it bootstraps, the action downloads exactly this asset and
# checks it against exactly this digest before declaring itself done,
# so fetching it directly verifies identically with nothing unproven
# left in the path. The digest is pinned inline for the reason the
# scanner's is: a checksums file fetched at run time only proves the
# asset matches whatever the release currently serves.
cosign_bin_dir="$RENDEZVOUS_WORK_DIR/bin"
mkdir -p "$cosign_bin_dir"
curl --fail --location --silent --show-error --output "$cosign_bin_dir/cosign" \
https://github.com/sigstore/cosign/releases/download/v3.0.6/cosign-linux-amd64
echo "c956e5dfcac53d52bcf058360d579472f0c1d2d9b69f55209e256fe7783f4c74 $cosign_bin_dir/cosign" \
| sha256sum --check --strict -
chmod +x "$cosign_bin_dir/cosign"
# publish-release.sh resolves cosign through `command -v`, so the
# directory joins the PATH the same way the action would have added it.
echo "$cosign_bin_dir" >>"$GITHUB_PATH"
"$cosign_bin_dir/cosign" version
- 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"