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.
312 lines
15 KiB
YAML
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"
|