Compare commits
5
Commits
c5bc9651e5
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3b687c903 | ||
|
|
747e3bb9c3 | ||
|
|
b235670afd | ||
|
|
562b8308b7 | ||
|
|
4233f12368 |
@@ -6,6 +6,7 @@
|
||||
.agents
|
||||
**/bin
|
||||
**/obj
|
||||
.release-work
|
||||
TestResults
|
||||
deploy/compose/secrets
|
||||
deploy/compose/.smoke.env
|
||||
|
||||
+112
-61
@@ -20,38 +20,36 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Resolve host bind sources for sibling containers
|
||||
- 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. Resolve them once from the runner's own
|
||||
# mounts and reuse them in every later step.
|
||||
# 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 workspace and temp paths as host paths."
|
||||
echo "Runner is not containerized; using the workspace path as its own host path."
|
||||
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")"
|
||||
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"
|
||||
echo "RENDEZVOUS_TEMP_SOURCE=$temp_source" >>"$GITHUB_ENV"
|
||||
|
||||
- name: Install pinned .NET SDKs
|
||||
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
|
||||
@@ -94,20 +92,19 @@ jobs:
|
||||
--target release-builder \
|
||||
--load \
|
||||
--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 \
|
||||
--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 \
|
||||
--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" "${RUNNER_TEMP}/release/$version"
|
||||
./scripts/verify-real-consumers.sh "$version" "${RUNNER_TEMP}/release/$version"
|
||||
./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=${RUNNER_TEMP}/release/$version" >>"$GITHUB_ENV"
|
||||
echo "RENDEZVOUS_RELEASE_DIR_SOURCE=${RENDEZVOUS_TEMP_SOURCE}/release/$version" >>"$GITHUB_ENV"
|
||||
echo "RENDEZVOUS_RELEASE_DIR=$release_dir" >>"$GITHUB_ENV"
|
||||
echo "RENDEZVOUS_RELEASE_BUILDER=$release_builder" >>"$GITHUB_ENV"
|
||||
|
||||
- name: Build exact container candidate
|
||||
@@ -124,8 +121,10 @@ jobs:
|
||||
--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"
|
||||
# .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" \
|
||||
@@ -136,9 +135,8 @@ jobs:
|
||||
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 \
|
||||
--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" \
|
||||
@@ -184,8 +182,8 @@ jobs:
|
||||
--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 \
|
||||
--volume "$runner_workspace_source:$GITHUB_WORKSPACE:ro" \
|
||||
--workdir "$GITHUB_WORKSPACE" \
|
||||
"$RENDEZVOUS_RELEASE_BUILDER" \
|
||||
bash -lc '
|
||||
for attempt in {1..180}; do
|
||||
@@ -198,23 +196,52 @@ jobs:
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
@@ -223,9 +250,8 @@ jobs:
|
||||
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 \
|
||||
--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" \
|
||||
@@ -236,7 +262,13 @@ jobs:
|
||||
_ "$RENDEZVOUS_RELEASE_DIR" "$RENDEZVOUS_VERSION" "$GITHUB_SHA" "$source_date_epoch"
|
||||
|
||||
- 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:
|
||||
name: rendezvous-${{ env.RENDEZVOUS_VERSION }}
|
||||
path: ${{ env.RENDEZVOUS_RELEASE_DIR }}
|
||||
@@ -244,9 +276,29 @@ jobs:
|
||||
retention-days: 30
|
||||
|
||||
- name: Install pinned signing client
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
with:
|
||||
cosign-release: v3.0.6
|
||||
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
|
||||
@@ -256,5 +308,4 @@ jobs:
|
||||
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"
|
||||
|
||||
@@ -8,6 +8,7 @@ TestResults/
|
||||
*.userosscache
|
||||
deploy/compose/.smoke.env
|
||||
artifacts/
|
||||
.release-work/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
deploy/compose/secrets/*
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ RUN dotnet publish src/FinalFactory.Rendezvous.Server/FinalFactory.Rendezvous.Se
|
||||
/p:RepositoryCommit="$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 \
|
||||
DOTNET_EnableDiagnostics=0 \
|
||||
|
||||
@@ -30,7 +30,10 @@ def load_json(path: pathlib.Path):
|
||||
def dependency_inventory(root: pathlib.Path):
|
||||
dependencies = {}
|
||||
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
|
||||
lock = load_json(lock_path)
|
||||
for framework in lock.get("dependencies", {}).values():
|
||||
|
||||
@@ -11,11 +11,21 @@ token="${RENDEZVOUS_RELEASE_TOKEN:?RENDEZVOUS_RELEASE_TOKEN is required}"
|
||||
username="${RENDEZVOUS_RELEASE_USERNAME:?RENDEZVOUS_RELEASE_USERNAME 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 bind sources must be
|
||||
# host paths. The workflow resolves them once and exports them; a direct host
|
||||
# run keeps the local paths.
|
||||
# `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}"
|
||||
release_dir_source="${RENDEZVOUS_RELEASE_DIR_SOURCE:-$release_dir}"
|
||||
[[ -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)"
|
||||
curl_config="$(mktemp)"
|
||||
release_request=""
|
||||
@@ -52,9 +62,8 @@ cosign public-key --key env://COSIGN_PRIVATE_KEY >/dev/null || {
|
||||
run_release_builder() {
|
||||
docker run --rm \
|
||||
--user "$(id -u):$(id -g)" \
|
||||
--volume "$workspace_source:/source:ro" \
|
||||
--volume "$release_dir_source:$release_dir" \
|
||||
--workdir /source \
|
||||
--volume "$workspace_source:$root" \
|
||||
--workdir "$root" \
|
||||
"$release_builder" "$@"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.CompilerServices;
|
||||
using FinalFactory.Rendezvous.Client;
|
||||
using FinalFactory.Rendezvous.Contracts;
|
||||
using FinalFactory.Rendezvous.Server.Abuse;
|
||||
@@ -146,13 +147,19 @@ public sealed class RendezvousClientIntegrationTests
|
||||
public async Task BrowserStreamResetsInvalidCursorReplaysReconnectAndReleasesConnections()
|
||||
{
|
||||
await using ClientTestHost host = await ClientTestHost.StartAsync();
|
||||
RendezvousPublisherClient publisher = new(host.HttpClient);
|
||||
RendezvousSessionBrowserClient browser = new(
|
||||
host.HttpClient,
|
||||
new RendezvousClientOptions
|
||||
{
|
||||
RequestTimeout = TimeSpan.FromSeconds(15),
|
||||
});
|
||||
// Budgets are sized for the release pipeline, not for a developer machine. That job
|
||||
// runs this suite inside a builder container on a busy act_runner host, alongside
|
||||
// parallel image builds, so the in-process Kestrel host and the HttpClient driving it
|
||||
// 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(30),
|
||||
};
|
||||
RendezvousPublisherClient publisher = new(host.HttpClient, loadedRunner);
|
||||
RendezvousSessionBrowserClient browser = new(host.HttpClient, loadedRunner);
|
||||
PublishedSession session = AssertSuccess(await publisher.RegisterAsync(
|
||||
CreateRegistration(200),
|
||||
host.PublisherCredential));
|
||||
@@ -161,18 +168,22 @@ public sealed class RendezvousClientIntegrationTests
|
||||
BrowseSessionsResponse snapshot = AssertSuccess(await browser.BrowseAsync(request));
|
||||
Assert.False(string.IsNullOrWhiteSpace(snapshot.StreamCursor));
|
||||
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(30));
|
||||
await using (IAsyncEnumerator<RendezvousClientResult<SessionStreamEvent>> invalid = browser
|
||||
.StreamAsync(request, CorruptCursor(snapshot.StreamCursor), timeout.Token)
|
||||
using CancellationTokenSource timeout = new(TimeSpan.FromMinutes(2));
|
||||
await using (IAsyncEnumerator<RendezvousClientResult<SessionStreamEvent>> invalid =
|
||||
StreamWithOpenRetry(
|
||||
browser,
|
||||
request,
|
||||
CorruptCursor(snapshot.StreamCursor),
|
||||
timeout.Token)
|
||||
.GetAsyncEnumerator(timeout.Token))
|
||||
{
|
||||
Assert.True(await invalid.MoveNextAsync());
|
||||
Assert.Equal(SessionStreamEventKind.Reset, AssertSuccess(invalid.Current).Kind);
|
||||
Assert.False(await invalid.MoveNextAsync());
|
||||
}
|
||||
await using IAsyncEnumerator<RendezvousClientResult<SessionStreamEvent>> events = browser
|
||||
.StreamAsync(request, snapshot.StreamCursor, timeout.Token)
|
||||
.GetAsyncEnumerator(timeout.Token);
|
||||
await using IAsyncEnumerator<RendezvousClientResult<SessionStreamEvent>> events =
|
||||
StreamWithOpenRetry(browser, request, snapshot.StreamCursor, timeout.Token)
|
||||
.GetAsyncEnumerator(timeout.Token);
|
||||
Task<bool> upsertPending = events.MoveNextAsync().AsTask();
|
||||
Assert.True((await publisher.UpdateAsync(
|
||||
session,
|
||||
@@ -190,8 +201,8 @@ public sealed class RendezvousClientIntegrationTests
|
||||
Assert.Equal(SessionStreamEventKind.SessionUpsert, upsert.Kind);
|
||||
Assert.Equal("Live update", upsert.Session!.DisplayName);
|
||||
|
||||
await using (IAsyncEnumerator<RendezvousClientResult<SessionStreamEvent>> replay = browser
|
||||
.StreamAsync(request, snapshot.StreamCursor, timeout.Token)
|
||||
await using (IAsyncEnumerator<RendezvousClientResult<SessionStreamEvent>> replay =
|
||||
StreamWithOpenRetry(browser, request, snapshot.StreamCursor, timeout.Token)
|
||||
.GetAsyncEnumerator(timeout.Token))
|
||||
{
|
||||
Assert.True(await replay.MoveNextAsync());
|
||||
@@ -212,6 +223,68 @@ public sealed class RendezvousClientIntegrationTests
|
||||
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)
|
||||
{
|
||||
char replacement = cursor[^1] == 'a' ? 'b' : 'a';
|
||||
|
||||
@@ -86,8 +86,50 @@ public sealed class ReleaseCompatibilityTests
|
||||
Assert.DoesNotContain("skip-duplicate", workflow, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("check-compatibility.sh", workflow, StringComparison.Ordinal);
|
||||
Assert.Contains("--platform linux/amd64", workflow, StringComparison.Ordinal);
|
||||
Assert.Contains("ignore-unfixed: false", workflow, StringComparison.Ordinal);
|
||||
Assert.Contains("format: spdx-json", workflow, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("aquasecurity/trivy-action", 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("finalize-release-candidate.sh", workflow, StringComparison.Ordinal);
|
||||
Assert.Contains("verify-real-consumers.sh", workflow, StringComparison.Ordinal);
|
||||
|
||||
Reference in New Issue
Block a user