Compare commits
8
Commits
589f802e2e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13382531ce | ||
|
|
e3b687c903 | ||
|
|
747e3bb9c3 | ||
|
|
b235670afd | ||
|
|
562b8308b7 | ||
|
|
4233f12368 | ||
|
|
c5bc9651e5 | ||
|
|
d41fa0c7e9 |
@@ -6,6 +6,7 @@
|
|||||||
.agents
|
.agents
|
||||||
**/bin
|
**/bin
|
||||||
**/obj
|
**/obj
|
||||||
|
.release-work
|
||||||
TestResults
|
TestResults
|
||||||
deploy/compose/secrets
|
deploy/compose/secrets
|
||||||
deploy/compose/.smoke.env
|
deploy/compose/.smoke.env
|
||||||
|
|||||||
+131
-41
@@ -20,6 +20,37 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
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
|
- name: Install pinned .NET SDKs
|
||||||
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
||||||
with:
|
with:
|
||||||
@@ -36,14 +67,19 @@ jobs:
|
|||||||
|
|
||||||
- name: Validate tag and produce reproducible artifacts
|
- name: Validate tag and produce reproducible artifacts
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
RENDEZVOUS_RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
version="${GITHUB_REF_NAME#v}"
|
version="${GITHUB_REF_NAME#v}"
|
||||||
./scripts/check-release-tag.sh "$GITHUB_REF_NAME"
|
./scripts/check-release-tag.sh "$GITHUB_REF_NAME"
|
||||||
previous_tag="$(git tag --merged HEAD^ --list 'v*.*.*' --sort=-version:refname | sed -n '1p')"
|
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
|
if [[ -n "$previous_tag" ]]; then
|
||||||
./scripts/check-compatibility.sh "$previous_tag"
|
./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
|
echo "No prior release tag is an ancestor of $GITHUB_REF_NAME." >&2
|
||||||
exit 1
|
exit 1
|
||||||
else
|
else
|
||||||
@@ -56,19 +92,19 @@ jobs:
|
|||||||
--target release-builder \
|
--target release-builder \
|
||||||
--load \
|
--load \
|
||||||
--tag "$release_builder" .
|
--tag "$release_builder" .
|
||||||
mkdir -p "${RUNNER_TEMP}/release-home" "${RUNNER_TEMP}/nuget"
|
release_dir="$RENDEZVOUS_WORK_DIR/release/$version"
|
||||||
|
mkdir -p "$RENDEZVOUS_WORK_DIR/release-home" "$RENDEZVOUS_WORK_DIR/nuget"
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
--user "$(id -u):$(id -g)" \
|
--user "$(id -u):$(id -g)" \
|
||||||
--env HOME="${RUNNER_TEMP}/release-home" \
|
--env HOME="$RENDEZVOUS_WORK_DIR/release-home" \
|
||||||
--env NUGET_PACKAGES="${RUNNER_TEMP}/nuget" \
|
--env NUGET_PACKAGES="$RENDEZVOUS_WORK_DIR/nuget" \
|
||||||
--volume "$GITHUB_WORKSPACE:/source" \
|
--volume "$RENDEZVOUS_WORKSPACE_SOURCE:$GITHUB_WORKSPACE" \
|
||||||
--volume "${RUNNER_TEMP}:${RUNNER_TEMP}" \
|
--workdir "$GITHUB_WORKSPACE" \
|
||||||
--workdir /source \
|
|
||||||
"$release_builder" \
|
"$release_builder" \
|
||||||
./scripts/build-release.sh "$version" "${RUNNER_TEMP}/release/$version"
|
./scripts/build-release.sh "$version" "$release_dir"
|
||||||
./scripts/verify-real-consumers.sh "$version" "${RUNNER_TEMP}/release/$version"
|
./scripts/verify-real-consumers.sh "$version" "$release_dir"
|
||||||
echo "RENDEZVOUS_VERSION=$version" >>"$GITHUB_ENV"
|
echo "RENDEZVOUS_VERSION=$version" >>"$GITHUB_ENV"
|
||||||
echo "RENDEZVOUS_RELEASE_DIR=${RUNNER_TEMP}/release/$version" >>"$GITHUB_ENV"
|
echo "RENDEZVOUS_RELEASE_DIR=$release_dir" >>"$GITHUB_ENV"
|
||||||
echo "RENDEZVOUS_RELEASE_BUILDER=$release_builder" >>"$GITHUB_ENV"
|
echo "RENDEZVOUS_RELEASE_BUILDER=$release_builder" >>"$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Build exact container candidate
|
- name: Build exact container candidate
|
||||||
@@ -85,8 +121,10 @@ jobs:
|
|||||||
--build-arg SOURCE_REVISION_ID="$GITHUB_SHA"
|
--build-arg SOURCE_REVISION_ID="$GITHUB_SHA"
|
||||||
)
|
)
|
||||||
release_tag="git.finalfactory.de/heikyu/rendezvous:${RENDEZVOUS_VERSION}"
|
release_tag="git.finalfactory.de/heikyu/rendezvous:${RENDEZVOUS_VERSION}"
|
||||||
image_one="${RUNNER_TEMP}/rendezvous-image-1.tar"
|
# .release-work is excluded from the build context, so the tar written
|
||||||
image_two="${RUNNER_TEMP}/rendezvous-image-2.tar"
|
# 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" \
|
docker buildx build "${common[@]}" --tag "$release_tag" \
|
||||||
--output "type=docker,dest=$image_one,rewrite-timestamp=true" .
|
--output "type=docker,dest=$image_one,rewrite-timestamp=true" .
|
||||||
docker buildx build "${common[@]}" --tag "$release_tag" \
|
docker buildx build "${common[@]}" --tag "$release_tag" \
|
||||||
@@ -97,9 +135,8 @@ jobs:
|
|||||||
buildkit_version="$(docker buildx inspect --bootstrap | sed -n 's/.*BuildKit version: *//p' | sed -n '1p')"
|
buildkit_version="$(docker buildx inspect --bootstrap | sed -n 's/.*BuildKit version: *//p' | sed -n '1p')"
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
--user "$(id -u):$(id -g)" \
|
--user "$(id -u):$(id -g)" \
|
||||||
--volume "$GITHUB_WORKSPACE:/source" \
|
--volume "$RENDEZVOUS_WORKSPACE_SOURCE:$GITHUB_WORKSPACE" \
|
||||||
--volume "${RUNNER_TEMP}:${RUNNER_TEMP}" \
|
--workdir "$GITHUB_WORKSPACE" \
|
||||||
--workdir /source \
|
|
||||||
"$RENDEZVOUS_RELEASE_BUILDER" \
|
"$RENDEZVOUS_RELEASE_BUILDER" \
|
||||||
python3 eng/release_artifacts.py record-container-build \
|
python3 eng/release_artifacts.py record-container-build \
|
||||||
--provenance "$RENDEZVOUS_RELEASE_DIR/release-provenance.json" \
|
--provenance "$RENDEZVOUS_RELEASE_DIR/release-provenance.json" \
|
||||||
@@ -131,9 +168,7 @@ jobs:
|
|||||||
port_suffix="$(( ${GITHUB_RUN_ID:-$$} % 10000 ))"
|
port_suffix="$(( ${GITHUB_RUN_ID:-$$} % 10000 ))"
|
||||||
export RENDEZVOUS_HTTP_HOST_PORT="$(( 20000 + port_suffix ))"
|
export RENDEZVOUS_HTTP_HOST_PORT="$(( 20000 + port_suffix ))"
|
||||||
export RENDEZVOUS_UDP_HOST_PORT="$(( 40000 + port_suffix ))"
|
export RENDEZVOUS_UDP_HOST_PORT="$(( 40000 + port_suffix ))"
|
||||||
runner_workspace_source="$(docker inspect "$HOSTNAME" | jq -er \
|
runner_workspace_source="${RENDEZVOUS_WORKSPACE_SOURCE:?host workspace bind source was not resolved}"
|
||||||
--arg destination "$GITHUB_WORKSPACE" \
|
|
||||||
'.[0].Mounts[] | select(.Destination == $destination) | .Source')"
|
|
||||||
export RENDEZVOUS_CONFIG_SOURCE="$runner_workspace_source/deploy/compose/appsettings.Production.json"
|
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_SECRET_SOURCE="$runner_workspace_source/deploy/compose/secrets/signing-key"
|
||||||
export RENDEZVOUS_IMAGE="git.finalfactory.de/heikyu/rendezvous:${RENDEZVOUS_VERSION}"
|
export RENDEZVOUS_IMAGE="git.finalfactory.de/heikyu/rendezvous:${RENDEZVOUS_VERSION}"
|
||||||
@@ -147,8 +182,8 @@ jobs:
|
|||||||
--env RENDEZVOUS_SMOKE_HTTP_URL=http://127.0.0.1:8080/ \
|
--env RENDEZVOUS_SMOKE_HTTP_URL=http://127.0.0.1:8080/ \
|
||||||
--env RENDEZVOUS_SMOKE_UDP_ENDPOINT=127.0.0.1:9050 \
|
--env RENDEZVOUS_SMOKE_UDP_ENDPOINT=127.0.0.1:9050 \
|
||||||
--env RENDEZVOUS_PUBLISHER_CREDENTIAL \
|
--env RENDEZVOUS_PUBLISHER_CREDENTIAL \
|
||||||
--volume "$runner_workspace_source:/source:ro" \
|
--volume "$runner_workspace_source:$GITHUB_WORKSPACE:ro" \
|
||||||
--workdir /source \
|
--workdir "$GITHUB_WORKSPACE" \
|
||||||
"$RENDEZVOUS_RELEASE_BUILDER" \
|
"$RENDEZVOUS_RELEASE_BUILDER" \
|
||||||
bash -lc '
|
bash -lc '
|
||||||
for attempt in {1..180}; do
|
for attempt in {1..180}; do
|
||||||
@@ -161,23 +196,52 @@ jobs:
|
|||||||
exit 1
|
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
|
- name: Scan candidate for high and critical vulnerabilities
|
||||||
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0, post-incident safe SHA
|
shell: bash
|
||||||
with:
|
run: |
|
||||||
image-ref: git.finalfactory.de/heikyu/rendezvous:${{ env.RENDEZVOUS_VERSION }}
|
set -euo pipefail
|
||||||
version: v0.69.3
|
# Unfixed vulnerabilities stay in scope: the flag that would drop them
|
||||||
format: table
|
# is never passed, so the gate keeps trivy's fail-closed default.
|
||||||
exit-code: "1"
|
"$RENDEZVOUS_TRIVY" image \
|
||||||
ignore-unfixed: false
|
--exit-code 1 \
|
||||||
severity: HIGH,CRITICAL
|
--severity HIGH,CRITICAL \
|
||||||
|
--format table \
|
||||||
|
"git.finalfactory.de/heikyu/rendezvous:${RENDEZVOUS_VERSION}"
|
||||||
|
|
||||||
- name: Generate container SPDX inventory
|
- name: Generate container SPDX inventory
|
||||||
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0, post-incident safe SHA
|
shell: bash
|
||||||
with:
|
run: |
|
||||||
image-ref: git.finalfactory.de/heikyu/rendezvous:${{ env.RENDEZVOUS_VERSION }}
|
set -euo pipefail
|
||||||
version: v0.69.3
|
"$RENDEZVOUS_TRIVY" image \
|
||||||
format: spdx-json
|
--format spdx-json \
|
||||||
output: ${{ env.RENDEZVOUS_RELEASE_DIR }}/FinalFactory.Rendezvous.Container.${{ env.RENDEZVOUS_VERSION }}.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
|
- name: Finalize checksums over the publish-ready candidate
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -186,9 +250,8 @@ jobs:
|
|||||||
source_date_epoch="$(git show -s --format=%ct HEAD)"
|
source_date_epoch="$(git show -s --format=%ct HEAD)"
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
--user "$(id -u):$(id -g)" \
|
--user "$(id -u):$(id -g)" \
|
||||||
--volume "$GITHUB_WORKSPACE:/source" \
|
--volume "$RENDEZVOUS_WORKSPACE_SOURCE:$GITHUB_WORKSPACE" \
|
||||||
--volume "${RUNNER_TEMP}:${RUNNER_TEMP}" \
|
--workdir "$GITHUB_WORKSPACE" \
|
||||||
--workdir /source \
|
|
||||||
"$RENDEZVOUS_RELEASE_BUILDER" \
|
"$RENDEZVOUS_RELEASE_BUILDER" \
|
||||||
bash -c 'python3 eng/release_artifacts.py normalize-container-sbom \
|
bash -c 'python3 eng/release_artifacts.py normalize-container-sbom \
|
||||||
--file "$1/FinalFactory.Rendezvous.Container.$2.spdx.json" \
|
--file "$1/FinalFactory.Rendezvous.Container.$2.spdx.json" \
|
||||||
@@ -199,7 +262,13 @@ jobs:
|
|||||||
_ "$RENDEZVOUS_RELEASE_DIR" "$RENDEZVOUS_VERSION" "$GITHUB_SHA" "$source_date_epoch"
|
_ "$RENDEZVOUS_RELEASE_DIR" "$RENDEZVOUS_VERSION" "$GITHUB_SHA" "$source_date_epoch"
|
||||||
|
|
||||||
- name: Preserve verified candidate artifacts
|
- name: Preserve verified candidate artifacts
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
# 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:
|
with:
|
||||||
name: rendezvous-${{ env.RENDEZVOUS_VERSION }}
|
name: rendezvous-${{ env.RENDEZVOUS_VERSION }}
|
||||||
path: ${{ env.RENDEZVOUS_RELEASE_DIR }}
|
path: ${{ env.RENDEZVOUS_RELEASE_DIR }}
|
||||||
@@ -207,9 +276,29 @@ jobs:
|
|||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|
||||||
- name: Install pinned signing client
|
- name: Install pinned signing client
|
||||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
shell: bash
|
||||||
with:
|
run: |
|
||||||
cosign-release: v3.0.6
|
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
|
- name: Publish once, sign, attest, and create release
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -218,4 +307,5 @@ jobs:
|
|||||||
RENDEZVOUS_RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
RENDEZVOUS_RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
|
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
|
||||||
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
|
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
|
||||||
|
RENDEZVOUS_WORKSPACE_SOURCE: ${{ env.RENDEZVOUS_WORKSPACE_SOURCE }}
|
||||||
run: ./scripts/publish-release.sh "$RENDEZVOUS_VERSION" "$RENDEZVOUS_RELEASE_DIR"
|
run: ./scripts/publish-release.sh "$RENDEZVOUS_VERSION" "$RENDEZVOUS_RELEASE_DIR"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ TestResults/
|
|||||||
*.userosscache
|
*.userosscache
|
||||||
deploy/compose/.smoke.env
|
deploy/compose/.smoke.env
|
||||||
artifacts/
|
artifacts/
|
||||||
|
.release-work/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
deploy/compose/secrets/*
|
deploy/compose/secrets/*
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ All notable Rendezvous release changes are recorded here. Versions follow
|
|||||||
Semantic Versioning; HTTP, UDP, and connection-ticket format compatibility is
|
Semantic Versioning; HTTP, UDP, and connection-ticket format compatibility is
|
||||||
tracked separately and called out for every release.
|
tracked separately and called out for every release.
|
||||||
|
|
||||||
## 1.0.0 - 2026-07-16
|
## 1.0.0 - 2026-08-22
|
||||||
|
|
||||||
### Compatibility
|
### Compatibility
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ RUN dotnet publish src/FinalFactory.Rendezvous.Server/FinalFactory.Rendezvous.Se
|
|||||||
/p:RepositoryCommit="$SOURCE_REVISION_ID" \
|
/p:RepositoryCommit="$SOURCE_REVISION_ID" \
|
||||||
/p:SourceRevisionId="$SOURCE_REVISION_ID"
|
/p:SourceRevisionId="$SOURCE_REVISION_ID"
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0.9-noble-chiseled@sha256:f820c4fbfb8bb204c3bbe05c69d48cd039cd0e67aa8f13ac1cec168819b90643 AS runtime
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0.11-noble-chiseled@sha256:0839314d08bb65da369135389a5d8291f75ace587fbb0488f469eb92c62eef68 AS runtime
|
||||||
|
|
||||||
ENV ASPNETCORE_HTTP_PORTS=8080 \
|
ENV ASPNETCORE_HTTP_PORTS=8080 \
|
||||||
DOTNET_EnableDiagnostics=0 \
|
DOTNET_EnableDiagnostics=0 \
|
||||||
|
|||||||
@@ -34,4 +34,8 @@ services:
|
|||||||
- ${RENDEZVOUS_SECRET_SOURCE:-./secrets/signing-key}:/run/secrets/rendezvous-signing-key:ro
|
- ${RENDEZVOUS_SECRET_SOURCE:-./secrets/signing-key}:/run/secrets/rendezvous-signing-key:ro
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${RENDEZVOUS_HTTP_HOST_PORT:-8080}:8080/tcp"
|
- "127.0.0.1:${RENDEZVOUS_HTTP_HOST_PORT:-8080}:8080/tcp"
|
||||||
|
# 9050 is also a common game-client default UDP port. Evaluating this
|
||||||
|
# service on the same machine as a running game client can silently
|
||||||
|
# collide; remap with RENDEZVOUS_UDP_HOST_PORT and keep the advertised
|
||||||
|
# Rendezvous:Deployment:PublicUdpPort in appsettings matched to it.
|
||||||
- "${RENDEZVOUS_UDP_HOST_PORT:-9050}:9050/udp"
|
- "${RENDEZVOUS_UDP_HOST_PORT:-9050}:9050/udp"
|
||||||
|
|||||||
@@ -31,8 +31,11 @@ must be repeated from the public feed.
|
|||||||
|
|
||||||
## Proven local path
|
## Proven local path
|
||||||
|
|
||||||
The SpaceGame host and client each create one caller-owned `NetManager`, set its
|
The SpaceGame host and client each create one caller-owned `NetManager`,
|
||||||
three gameplay QoS channels before `Start`, and give the same manager and
|
configure any additional gameplay QoS channels before `Start` (the pilot
|
||||||
|
harness used three; current SpaceGame source uses LiteNetLib's default single
|
||||||
|
channel — both peers must simply agree, because a `ChannelsCount` mismatch
|
||||||
|
fails the connection silently), and give the same manager and
|
||||||
`RendezvousNetListener` to the coordinator. Rendezvous authenticates discovery,
|
`RendezvousNetListener` to the coordinator. Rendezvous authenticates discovery,
|
||||||
join authorization, host presence, mediation, and connection outcome reporting.
|
join authorization, host presence, mediation, and connection outcome reporting.
|
||||||
After traversal, SpaceGame performs a separate audience-bound admission exchange
|
After traversal, SpaceGame performs a separate audience-bound admission exchange
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ def load_json(path: pathlib.Path):
|
|||||||
def dependency_inventory(root: pathlib.Path):
|
def dependency_inventory(root: pathlib.Path):
|
||||||
dependencies = {}
|
dependencies = {}
|
||||||
for lock_path in sorted(root.glob("**/packages.lock.json")):
|
for lock_path in sorted(root.glob("**/packages.lock.json")):
|
||||||
if any(part in {"bin", "obj", "artifacts"} for part in lock_path.parts):
|
if any(
|
||||||
|
part in {"bin", "obj", "artifacts", ".release-work"}
|
||||||
|
for part in lock_path.parts
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
lock = load_json(lock_path)
|
lock = load_json(lock_path)
|
||||||
for framework in lock.get("dependencies", {}).values():
|
for framework in lock.get("dependencies", {}).values():
|
||||||
|
|||||||
@@ -10,6 +10,22 @@ image="$registry/heikyu/rendezvous:$version"
|
|||||||
token="${RENDEZVOUS_RELEASE_TOKEN:?RENDEZVOUS_RELEASE_TOKEN is required}"
|
token="${RENDEZVOUS_RELEASE_TOKEN:?RENDEZVOUS_RELEASE_TOKEN is required}"
|
||||||
username="${RENDEZVOUS_RELEASE_USERNAME:?RENDEZVOUS_RELEASE_USERNAME is required}"
|
username="${RENDEZVOUS_RELEASE_USERNAME:?RENDEZVOUS_RELEASE_USERNAME is required}"
|
||||||
release_builder="${RENDEZVOUS_RELEASE_BUILDER:?RENDEZVOUS_RELEASE_BUILDER 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 the bind source must be
|
||||||
|
# a host path. The workflow resolves the workspace once and exports it; a direct
|
||||||
|
# host run keeps the local path. Only the workspace is bound, so the release
|
||||||
|
# directory has to live inside it, and binding it back onto its own path keeps
|
||||||
|
# every shared path identical on both sides of the boundary.
|
||||||
|
workspace_source="${RENDEZVOUS_WORKSPACE_SOURCE:-$root}"
|
||||||
|
[[ -d "$release_dir" ]] || {
|
||||||
|
echo "Release directory does not exist: $release_dir" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
release_dir="$(cd "$release_dir" && pwd)"
|
||||||
|
if [[ "$release_dir" != "$root"/* ]]; then
|
||||||
|
echo "Release directory must live inside the workspace: $release_dir" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
docker_config="$(mktemp -d)"
|
docker_config="$(mktemp -d)"
|
||||||
curl_config="$(mktemp)"
|
curl_config="$(mktemp)"
|
||||||
release_request=""
|
release_request=""
|
||||||
@@ -33,12 +49,21 @@ for command in cosign curl docker dotnet jq; do
|
|||||||
}
|
}
|
||||||
done
|
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() {
|
run_release_builder() {
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
--user "$(id -u):$(id -g)" \
|
--user "$(id -u):$(id -g)" \
|
||||||
--volume "$root:/source:ro" \
|
--volume "$workspace_source:$root" \
|
||||||
--volume "$release_dir:$release_dir" \
|
--workdir "$root" \
|
||||||
--workdir /source \
|
|
||||||
"$release_builder" "$@"
|
"$release_builder" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|||||||
version="${1:?usage: verify-real-consumers.sh VERSION RELEASE_DIRECTORY}"
|
version="${1:?usage: verify-real-consumers.sh VERSION RELEASE_DIRECTORY}"
|
||||||
release_dir="${2:?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"
|
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")"
|
work="$(mktemp -d "${TMPDIR:-/tmp}/rendezvous-consumers.XXXXXX")"
|
||||||
cleanup() {
|
cleanup() {
|
||||||
rm -rf "$work"
|
rm -rf "$work"
|
||||||
@@ -18,6 +23,20 @@ for command in dotnet git jq python3; do
|
|||||||
}
|
}
|
||||||
done
|
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 \
|
python3 "$root/eng/release_artifacts.py" consumer-config \
|
||||||
--local-source "$release_dir" \
|
--local-source "$release_dir" \
|
||||||
--output "$work/NuGet.config"
|
--output "$work/NuGet.config"
|
||||||
@@ -31,7 +50,7 @@ for ((index = 0; index < count; index++)); do
|
|||||||
checkout="$work/$name"
|
checkout="$work/$name"
|
||||||
git -c init.defaultBranch=main init --quiet "$checkout"
|
git -c init.defaultBranch=main init --quiet "$checkout"
|
||||||
git -C "$checkout" remote add origin "$repository"
|
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_LFS_SKIP_SMUDGE=1 git -C "$checkout" checkout --quiet --detach FETCH_HEAD
|
||||||
[[ "$(git -C "$checkout" rev-parse HEAD)" == "$revision" ]] || {
|
[[ "$(git -C "$checkout" rev-parse HEAD)" == "$revision" ]] || {
|
||||||
echo "$name did not resolve the pinned consumer revision." >&2
|
echo "$name did not resolve the pinned consumer revision." >&2
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using FinalFactory.Rendezvous.Client;
|
using FinalFactory.Rendezvous.Client;
|
||||||
using FinalFactory.Rendezvous.Contracts;
|
using FinalFactory.Rendezvous.Contracts;
|
||||||
using FinalFactory.Rendezvous.Server.Abuse;
|
using FinalFactory.Rendezvous.Server.Abuse;
|
||||||
@@ -146,13 +147,19 @@ public sealed class RendezvousClientIntegrationTests
|
|||||||
public async Task BrowserStreamResetsInvalidCursorReplaysReconnectAndReleasesConnections()
|
public async Task BrowserStreamResetsInvalidCursorReplaysReconnectAndReleasesConnections()
|
||||||
{
|
{
|
||||||
await using ClientTestHost host = await ClientTestHost.StartAsync();
|
await using ClientTestHost host = await ClientTestHost.StartAsync();
|
||||||
RendezvousPublisherClient publisher = new(host.HttpClient);
|
// Budgets are sized for the release pipeline, not for a developer machine. That job
|
||||||
RendezvousSessionBrowserClient browser = new(
|
// runs this suite inside a builder container on a busy act_runner host, alongside
|
||||||
host.HttpClient,
|
// parallel image builds, so the in-process Kestrel host and the HttpClient driving it
|
||||||
new RendezvousClientOptions
|
// share a thread pool that is routinely starved. A loopback round trip that costs
|
||||||
|
// microseconds locally can then cost seconds - and the first stream request in the
|
||||||
|
// process additionally pays the one-time JIT and serializer warm-up of the SSE path.
|
||||||
|
// 30s is the ceiling RendezvousClientOptions.Validate permits for RequestTimeout.
|
||||||
|
RendezvousClientOptions loadedRunner = new()
|
||||||
{
|
{
|
||||||
RequestTimeout = TimeSpan.FromSeconds(15),
|
RequestTimeout = TimeSpan.FromSeconds(30),
|
||||||
});
|
};
|
||||||
|
RendezvousPublisherClient publisher = new(host.HttpClient, loadedRunner);
|
||||||
|
RendezvousSessionBrowserClient browser = new(host.HttpClient, loadedRunner);
|
||||||
PublishedSession session = AssertSuccess(await publisher.RegisterAsync(
|
PublishedSession session = AssertSuccess(await publisher.RegisterAsync(
|
||||||
CreateRegistration(200),
|
CreateRegistration(200),
|
||||||
host.PublisherCredential));
|
host.PublisherCredential));
|
||||||
@@ -161,17 +168,21 @@ public sealed class RendezvousClientIntegrationTests
|
|||||||
BrowseSessionsResponse snapshot = AssertSuccess(await browser.BrowseAsync(request));
|
BrowseSessionsResponse snapshot = AssertSuccess(await browser.BrowseAsync(request));
|
||||||
Assert.False(string.IsNullOrWhiteSpace(snapshot.StreamCursor));
|
Assert.False(string.IsNullOrWhiteSpace(snapshot.StreamCursor));
|
||||||
|
|
||||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(30));
|
using CancellationTokenSource timeout = new(TimeSpan.FromMinutes(2));
|
||||||
await using (IAsyncEnumerator<RendezvousClientResult<SessionStreamEvent>> invalid = browser
|
await using (IAsyncEnumerator<RendezvousClientResult<SessionStreamEvent>> invalid =
|
||||||
.StreamAsync(request, CorruptCursor(snapshot.StreamCursor), timeout.Token)
|
StreamWithOpenRetry(
|
||||||
|
browser,
|
||||||
|
request,
|
||||||
|
CorruptCursor(snapshot.StreamCursor),
|
||||||
|
timeout.Token)
|
||||||
.GetAsyncEnumerator(timeout.Token))
|
.GetAsyncEnumerator(timeout.Token))
|
||||||
{
|
{
|
||||||
Assert.True(await invalid.MoveNextAsync());
|
Assert.True(await invalid.MoveNextAsync());
|
||||||
Assert.Equal(SessionStreamEventKind.Reset, AssertSuccess(invalid.Current).Kind);
|
Assert.Equal(SessionStreamEventKind.Reset, AssertSuccess(invalid.Current).Kind);
|
||||||
Assert.False(await invalid.MoveNextAsync());
|
Assert.False(await invalid.MoveNextAsync());
|
||||||
}
|
}
|
||||||
await using IAsyncEnumerator<RendezvousClientResult<SessionStreamEvent>> events = browser
|
await using IAsyncEnumerator<RendezvousClientResult<SessionStreamEvent>> events =
|
||||||
.StreamAsync(request, snapshot.StreamCursor, timeout.Token)
|
StreamWithOpenRetry(browser, request, snapshot.StreamCursor, timeout.Token)
|
||||||
.GetAsyncEnumerator(timeout.Token);
|
.GetAsyncEnumerator(timeout.Token);
|
||||||
Task<bool> upsertPending = events.MoveNextAsync().AsTask();
|
Task<bool> upsertPending = events.MoveNextAsync().AsTask();
|
||||||
Assert.True((await publisher.UpdateAsync(
|
Assert.True((await publisher.UpdateAsync(
|
||||||
@@ -190,8 +201,8 @@ public sealed class RendezvousClientIntegrationTests
|
|||||||
Assert.Equal(SessionStreamEventKind.SessionUpsert, upsert.Kind);
|
Assert.Equal(SessionStreamEventKind.SessionUpsert, upsert.Kind);
|
||||||
Assert.Equal("Live update", upsert.Session!.DisplayName);
|
Assert.Equal("Live update", upsert.Session!.DisplayName);
|
||||||
|
|
||||||
await using (IAsyncEnumerator<RendezvousClientResult<SessionStreamEvent>> replay = browser
|
await using (IAsyncEnumerator<RendezvousClientResult<SessionStreamEvent>> replay =
|
||||||
.StreamAsync(request, snapshot.StreamCursor, timeout.Token)
|
StreamWithOpenRetry(browser, request, snapshot.StreamCursor, timeout.Token)
|
||||||
.GetAsyncEnumerator(timeout.Token))
|
.GetAsyncEnumerator(timeout.Token))
|
||||||
{
|
{
|
||||||
Assert.True(await replay.MoveNextAsync());
|
Assert.True(await replay.MoveNextAsync());
|
||||||
@@ -212,6 +223,68 @@ public sealed class RendezvousClientIntegrationTests
|
|||||||
Assert.Equal(session.ListingId, remove.ListingId);
|
Assert.Equal(session.ListingId, remove.ListingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Opening an event stream is a single, unretried request in the SDK, so a handshake that
|
||||||
|
// loses its wall-clock budget on a saturated runner surfaces as a typed ServiceUnavailable
|
||||||
|
// first element instead of the expected event. Reopening is semantically free: every call
|
||||||
|
// site passes a replayable snapshot cursor, so a reopened stream observes exactly the
|
||||||
|
// events the first attempt would have delivered.
|
||||||
|
//
|
||||||
|
// The retry cannot hide a product regression. StreamSessions never answers with a
|
||||||
|
// ServiceUnavailable envelope - its only rejections are InvalidRequest,
|
||||||
|
// UnsupportedContractVersion, CapacityExceeded and RateLimited - so this code path is
|
||||||
|
// reachable only from the transport catch in RendezvousHttpTransport.OpenStreamAsync,
|
||||||
|
// i.e. a timed-out or dropped handshake. Attempts are bounded, and the final failure is
|
||||||
|
// yielded verbatim, so a stream that is genuinely unopenable still fails the test with the
|
||||||
|
// original message.
|
||||||
|
private const int StreamOpenAttempts = 3;
|
||||||
|
|
||||||
|
private static async IAsyncEnumerable<RendezvousClientResult<SessionStreamEvent>> StreamWithOpenRetry(
|
||||||
|
RendezvousSessionBrowserClient browser,
|
||||||
|
BrowseSessionsRequest request,
|
||||||
|
string streamCursor,
|
||||||
|
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
for (int attempt = 1; ; attempt++)
|
||||||
|
{
|
||||||
|
bool reopen = false;
|
||||||
|
await using (IAsyncEnumerator<RendezvousClientResult<SessionStreamEvent>> source = browser
|
||||||
|
.StreamAsync(request, streamCursor, cancellationToken)
|
||||||
|
.GetAsyncEnumerator(cancellationToken))
|
||||||
|
{
|
||||||
|
bool opening = true;
|
||||||
|
while (await source.MoveNextAsync())
|
||||||
|
{
|
||||||
|
RendezvousClientResult<SessionStreamEvent> current = source.Current;
|
||||||
|
if (opening
|
||||||
|
&& !current.IsSuccess
|
||||||
|
&& current.Error == RendezvousErrorCode.ServiceUnavailable
|
||||||
|
&& attempt < StreamOpenAttempts)
|
||||||
|
{
|
||||||
|
reopen = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
opening = false;
|
||||||
|
// Keepalives are protocol filler with no session semantics. The server emits
|
||||||
|
// one whenever a subscription idles for its keepalive interval, which a slow
|
||||||
|
// runner reaches between the assertions below; dropping them keeps the
|
||||||
|
// reset/upsert/remove expectations exact.
|
||||||
|
if (current.IsSuccess && current.Value!.Kind == SessionStreamEventKind.Keepalive)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reopen)
|
||||||
|
{
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static string CorruptCursor(string cursor)
|
private static string CorruptCursor(string cursor)
|
||||||
{
|
{
|
||||||
char replacement = cursor[^1] == 'a' ? 'b' : 'a';
|
char replacement = cursor[^1] == 'a' ? 'b' : 'a';
|
||||||
|
|||||||
@@ -86,8 +86,50 @@ public sealed class ReleaseCompatibilityTests
|
|||||||
Assert.DoesNotContain("skip-duplicate", workflow, StringComparison.OrdinalIgnoreCase);
|
Assert.DoesNotContain("skip-duplicate", workflow, StringComparison.OrdinalIgnoreCase);
|
||||||
Assert.Contains("check-compatibility.sh", workflow, StringComparison.Ordinal);
|
Assert.Contains("check-compatibility.sh", workflow, StringComparison.Ordinal);
|
||||||
Assert.Contains("--platform linux/amd64", workflow, StringComparison.Ordinal);
|
Assert.Contains("--platform linux/amd64", workflow, StringComparison.Ordinal);
|
||||||
Assert.Contains("ignore-unfixed: false", workflow, StringComparison.Ordinal);
|
Assert.DoesNotContain("aquasecurity/trivy-action", workflow, StringComparison.Ordinal);
|
||||||
Assert.Contains("format: spdx-json", workflow, StringComparison.Ordinal);
|
Assert.Contains(
|
||||||
|
"https://github.com/aquasecurity/trivy/releases/download/v0.69.3/trivy_0.69.3_Linux-64bit.tar.gz",
|
||||||
|
workflow,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"1816b632dfe529869c740c0913e36bd1629cb7688bd5634f4a858c1d57c88b75",
|
||||||
|
workflow,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("sha256sum --check --strict", workflow, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("TRIVY_CACHE_DIR=$RENDEZVOUS_WORK_DIR/trivy-cache", workflow, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
// Gitea answers the v3-era artifacts API only, so upload-artifact stays on
|
||||||
|
// v3 and never gates the release it is only meant to help debug.
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02",
|
||||||
|
workflow,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3.2.1",
|
||||||
|
workflow,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("continue-on-error: true", workflow, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
// The signing client is fetched the way the scanner is: a pinned asset
|
||||||
|
// checked against a pinned digest, with no action resolved off github.com.
|
||||||
|
Assert.DoesNotContain("sigstore/cosign-installer", workflow, StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"https://github.com/sigstore/cosign/releases/download/v3.0.6/cosign-linux-amd64",
|
||||||
|
workflow,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"c956e5dfcac53d52bcf058360d579472f0c1d2d9b69f55209e256fe7783f4c74",
|
||||||
|
workflow,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("echo \"$cosign_bin_dir\" >>\"$GITHUB_PATH\"", workflow, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("--exit-code 1", workflow, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("--severity HIGH,CRITICAL", workflow, StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("--ignore-unfixed", workflow, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("--format spdx-json", workflow, StringComparison.Ordinal);
|
||||||
|
Assert.Contains(
|
||||||
|
"--output \"$RENDEZVOUS_RELEASE_DIR/FinalFactory.Rendezvous.Container.$RENDEZVOUS_VERSION.spdx.json\"",
|
||||||
|
workflow,
|
||||||
|
StringComparison.Ordinal);
|
||||||
Assert.Contains("normalize-container-sbom", workflow, StringComparison.Ordinal);
|
Assert.Contains("normalize-container-sbom", workflow, StringComparison.Ordinal);
|
||||||
Assert.Contains("finalize-release-candidate.sh", workflow, StringComparison.Ordinal);
|
Assert.Contains("finalize-release-candidate.sh", workflow, StringComparison.Ordinal);
|
||||||
Assert.Contains("verify-real-consumers.sh", workflow, StringComparison.Ordinal);
|
Assert.Contains("verify-real-consumers.sh", workflow, StringComparison.Ordinal);
|
||||||
|
|||||||
Reference in New Issue
Block a user