✨ feat(status): protect status and diagnostics (#664) #1175
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CodSpeed | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: codspeed-${{ github.event_name == 'push' && github.sha || github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| # Pull requests build affected benchmark legs. Pushes and manual runs refresh the complete baseline. | |
| changes: | |
| runs-on: ubuntu-24.04 | |
| outputs: | |
| bench_matrix: ${{ steps.matrix.outputs.include }} | |
| workflow_changed: ${{ steps.filter.outputs.workflow }} | |
| steps: | |
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 | |
| id: filter | |
| with: | |
| filters: | | |
| # Rust src/tests modules compile only under cfg(test), so they cannot affect benchmark binaries. | |
| shared: | |
| - 'crates/peryx-core/{Cargo.toml,build.rs,src/*,src/!(tests)/**,benches/**}' | |
| - 'crates/peryx-storage/{Cargo.toml,build.rs,src/*,src/!(tests)/**,benches/**}' | |
| - 'crates/peryx-http/{Cargo.toml,build.rs,src/*,src/!(tests)/**,benches/**}' | |
| - 'crates/peryx-driver/{Cargo.toml,build.rs,src/*,src/!(tests)/**,benches/**}' | |
| - 'crates/peryx-identity/{Cargo.toml,build.rs,src/*,src/!(tests)/**,benches/**}' | |
| - 'crates/peryx-policy/{Cargo.toml,build.rs,src/*,src/!(tests)/**,benches/**}' | |
| - 'crates/peryx-index/{Cargo.toml,build.rs,src/*,src/!(tests)/**,benches/**}' | |
| - 'crates/peryx-events/{Cargo.toml,build.rs,src/*,src/!(tests)/**,benches/**}' | |
| - 'crates/peryx-upstream/{Cargo.toml,build.rs,src/*,src/!(tests)/**,benches/**}' | |
| - 'crates/peryx-search/{Cargo.toml,build.rs,src/*,src/!(tests)/**,benches/**}' | |
| - 'crates/peryx-replication/{Cargo.toml,build.rs,src/*,src/!(tests)/**,benches/**}' | |
| - 'crates/peryx/{Cargo.toml,build.rs,src/*,src/!(tests)/**,benches/**}' | |
| - 'Cargo.toml' | |
| - 'Cargo.lock' | |
| - '.cargo/**' | |
| - 'rust-toolchain*' | |
| runner: | |
| - '.github/codspeed/**' | |
| - '.github/workflows/codspeed.yml' | |
| - 'ci/run-codspeed*.sh' | |
| workflow: | |
| - '.github/workflows/codspeed.yml' | |
| pypi: | |
| - 'crates/peryx-ecosystem-pypi/{Cargo.toml,build.rs,src/*,src/!(tests)/**,benches/**}' | |
| oci: | |
| - 'crates/peryx-ecosystem-oci/{Cargo.toml,build.rs,src/*,src/!(tests)/**,benches/**}' | |
| - id: matrix | |
| env: | |
| EVENT: ${{ github.event_name }} | |
| RUNNER: ${{ steps.filter.outputs.runner }} | |
| SHARED: ${{ steps.filter.outputs.shared }} | |
| OCI: ${{ steps.filter.outputs.oci }} | |
| PYPI: ${{ steps.filter.outputs.pypi }} | |
| run: | | |
| # The standard no-LTO benchmark profile keeps links short and within the standard ARM | |
| # runner's memory, so both ecosystems build in parallel. | |
| oci='{"ecosystem":"OCI","package":"peryx-ecosystem-oci","cargo_jobs":4}' | |
| pypi='{"ecosystem":"PyPI","package":"peryx-ecosystem-pypi","cargo_jobs":4}' | |
| legs=() | |
| if [ "$EVENT" == "push" ] || [ "$EVENT" == "workflow_dispatch" ]; then | |
| legs=("$oci" "$pypi") | |
| elif [ "$SHARED" = "true" ]; then | |
| legs=("$oci" "$pypi") | |
| else | |
| if [ "$OCI" = "true" ]; then legs+=("$oci"); fi | |
| if [ "$PYPI" = "true" ]; then legs+=("$pypi"); fi | |
| if [ ${#legs[@]} -eq 0 ] && [ "$RUNNER" = "true" ]; then legs+=("$pypi"); fi | |
| fi | |
| IFS=, | |
| echo "include=[${legs[*]}]" >> "$GITHUB_OUTPUT" | |
| baseline: | |
| name: Compatible baseline | |
| needs: changes | |
| if: >- | |
| github.event_name == 'pull_request' && | |
| needs.changes.outputs.bench_matrix != '[]' | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 20 | |
| permissions: | |
| actions: write | |
| contents: read | |
| outputs: | |
| compatible: ${{ steps.baseline.outputs.compatible }} | |
| steps: | |
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Find compatible baseline | |
| id: baseline | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| GH_REPO: ${{ github.repository }} | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| echo "compatible=false" >> "$GITHUB_OUTPUT" | |
| for attempt in {1..90}; do | |
| baseline=$( | |
| gh run list \ | |
| --repo "$GH_REPO" \ | |
| --workflow codspeed.yml \ | |
| --branch "$DEFAULT_BRANCH" \ | |
| --commit "$BASE_SHA" \ | |
| --event push \ | |
| --limit 10 \ | |
| --json conclusion,createdAt,headSha,status,url \ | |
| --jq 'sort_by(.createdAt) | last // {}' | |
| ) || exit 0 | |
| status=$(jq -r '.status // "missing"' <<< "$baseline") | |
| if [[ "$status" == completed ]]; then | |
| conclusion=$(jq -r '.conclusion' <<< "$baseline") | |
| if [[ "$conclusion" != success ]]; then | |
| echo "The exact base benchmark concluded: $conclusion" | |
| exit 0 | |
| fi | |
| echo "Found the exact base benchmark: $(jq -r '.url' <<< "$baseline")" | |
| break | |
| fi | |
| echo "Waiting for the exact base benchmark ($status, attempt $attempt/90)." | |
| sleep 10 | |
| done | |
| if [[ "$status" != completed ]]; then | |
| echo "The exact base benchmark did not complete within 15 minutes." | |
| exit 0 | |
| fi | |
| git cat-file -e "$BASE_SHA^{commit}" 2>/dev/null || git fetch --depth=1 origin "$BASE_SHA" | |
| runtime_id() { | |
| local revision=$1 | |
| local path | |
| { | |
| for path in \ | |
| .github/codspeed/Dockerfile \ | |
| .github/workflows/codspeed.yml \ | |
| ci/run-codspeed.sh; do | |
| printf '%s\0' "$path" | |
| git show "$revision:$path" | |
| printf '\0' | |
| done | |
| } | sha256sum | cut -d ' ' -f 1 | |
| } | |
| head_runtime=$(runtime_id HEAD) | |
| base_runtime=$(runtime_id "$BASE_SHA") | |
| if [[ "$head_runtime" == "$base_runtime" ]]; then | |
| echo "compatible=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "The pull request changes the benchmark runtime; results will not be uploaded." | |
| fi | |
| - name: Cancel remaining jobs on failure | |
| if: failure() && !cancelled() | |
| uses: ./.github/actions/cancel-on-failure | |
| codspeed-image: | |
| name: CodSpeed runtime image | |
| needs: changes | |
| if: needs.changes.outputs.bench_matrix != '[]' | |
| runs-on: ubuntu-24.04-arm | |
| timeout-minutes: 20 | |
| permissions: | |
| actions: write | |
| contents: read | |
| packages: write | |
| outputs: | |
| available: ${{ steps.image.outputs.available }} | |
| image: ${{ steps.image.outputs.image }} | |
| steps: | |
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 | |
| with: | |
| fetch-depth: 2 | |
| persist-credentials: false | |
| - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 | |
| - name: Resolve immutable runtime image | |
| id: image | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| EVENT: ${{ github.event_name }} | |
| GH_TOKEN: ${{ github.token }} | |
| IMAGE: ghcr.io/${{ github.repository_owner }}/peryx-codspeed | |
| run: | | |
| set -euo pipefail | |
| echo "$GH_TOKEN" | docker login ghcr.io --username "$GITHUB_ACTOR" --password-stdin | |
| definition=$(sha256sum .github/codspeed/Dockerfile | cut -d ' ' -f 1) | |
| tag="$IMAGE:definition-$definition" | |
| digest=$(docker buildx imagetools inspect "$tag" --format '{{.Manifest.Digest}}' 2>/dev/null || true) | |
| if [[ "$digest" == sha256:* ]]; then | |
| echo "available=true" >> "$GITHUB_OUTPUT" | |
| echo "image=$IMAGE@$digest" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [[ "$EVENT" == pull_request ]] && git cat-file -e "$BASE_SHA:.github/codspeed/Dockerfile"; then | |
| base_definition=$(git show "$BASE_SHA:.github/codspeed/Dockerfile" | sha256sum | cut -d ' ' -f 1) | |
| if [[ "$definition" == "$base_definition" ]]; then | |
| echo "The runtime image for this reviewed definition is unavailable." | |
| exit 1 | |
| fi | |
| fi | |
| build=( | |
| docker buildx build | |
| --file .github/codspeed/Dockerfile | |
| --platform linux/arm64 | |
| --tag "$tag" | |
| --label "org.opencontainers.image.revision=$GITHUB_SHA" | |
| ) | |
| if [[ "$EVENT" == pull_request ]]; then | |
| "${build[@]}" --load . | |
| echo "available=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| "${build[@]}" --provenance=mode=max --sbom=true --push . | |
| for attempt in {1..5}; do | |
| digest=$(docker buildx imagetools inspect "$tag" --format '{{.Manifest.Digest}}' 2>/dev/null || true) | |
| [[ "$digest" == sha256:* ]] && break | |
| sleep $((attempt * 2)) | |
| done | |
| [[ "$digest" == sha256:* ]] | |
| echo "available=true" >> "$GITHUB_OUTPUT" | |
| echo "image=$IMAGE@$digest" >> "$GITHUB_OUTPUT" | |
| - name: Cancel remaining jobs on failure | |
| if: failure() && !cancelled() | |
| uses: ./.github/actions/cancel-on-failure | |
| operations: | |
| name: ${{ matrix.ecosystem }} operation benchmarks | |
| needs: [changes, baseline, codspeed-image] | |
| if: >- | |
| always() && | |
| (needs.baseline.result == 'success' || needs.baseline.result == 'skipped') && | |
| needs.codspeed-image.result == 'success' && | |
| needs.codspeed-image.outputs.available == 'true' && | |
| needs.changes.outputs.bench_matrix != '[]' | |
| concurrency: | |
| # GitHub left ARM jobs running after workflow-level cancellation in #280. | |
| group: codspeed-${{ matrix.ecosystem }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }} | |
| cancel-in-progress: true | |
| strategy: | |
| fail-fast: true | |
| matrix: | |
| include: ${{ fromJSON(needs.changes.outputs.bench_matrix) }} | |
| runs-on: ubuntu-24.04-arm | |
| container: | |
| image: ${{ needs.codspeed-image.outputs.image }} # zizmor: ignore[unpinned-images] output is digest-pinned | |
| # CodSpeed's Valgrind runner uses setarch; Docker's default seccomp profile blocks personality. | |
| options: --security-opt seccomp=unconfined | |
| credentials: | |
| username: ${{ github.actor }} | |
| password: ${{ github.token }} | |
| defaults: | |
| run: | |
| shell: bash | |
| timeout-minutes: 30 | |
| permissions: | |
| actions: read | |
| contents: read | |
| id-token: write | |
| packages: read | |
| env: | |
| # GitHub hands out heterogeneous ARM CPUs for ubuntu-24.04-arm. glibc then picks CPU-specific | |
| # memcpy/memset/malloc paths per runner, so cachegrind (simulation mode) measures the same | |
| # benchmark differently and reports phantom regressions on unrelated benchmarks. Forcing the | |
| # generic aarch64 code path and a single malloc arena makes the measurement CPU-independent. | |
| # See CodSpeed's write-up on runner CPU variance (codspeed.io/blog/why-glibc-faster-github-actions). | |
| GLIBC_TUNABLES: glibc.cpu.name=generic:glibc.malloc.arena_max=1 | |
| # cargo codspeed builds with the release profile, whose project-wide fat LTO does a serial, | |
| # memory-heavy whole-program link. Cargo's standard optimized benchmark profile disables LTO; | |
| # use that here so changed sources remain quick to link. Production and dist builds keep LTO. | |
| CARGO_PROFILE_RELEASE_LTO: "false" | |
| CODSPEED_SKIP_UPLOAD: >- | |
| ${{ github.event_name == 'pull_request' && | |
| (needs.changes.outputs.workflow_changed == 'true' || needs.baseline.outputs.compatible != 'true') }} | |
| steps: | |
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Compute benchmark source key | |
| id: source | |
| run: | | |
| git config --global --add safe.directory "$(pwd)" | |
| git ls-files -s -- \ | |
| .cargo \ | |
| .github/codspeed \ | |
| .github/workflows/codspeed.yml \ | |
| ci/run-codspeed.sh \ | |
| crates \ | |
| Cargo.lock \ | |
| Cargo.toml \ | |
| rust-toolchain.toml \ | |
| | sha256sum \ | |
| | sed 's/ .*//' \ | |
| | xargs -I {} echo "key={}" >> "$GITHUB_OUTPUT" | |
| # rust-cache removes workspace outputs before saving. That made an exact 848 MB cache hit | |
| # recompile and relink every benchmark for eight minutes. Retain the complete CodSpeed target; | |
| # source-specific keys make main reruns immediate, while pull requests restore the newest main | |
| # artifacts for incremental builds without filling the cache with merge-ref-only archives. | |
| - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| id: benchmark-cache | |
| with: | |
| path: | | |
| /usr/local/cargo/git/db | |
| /usr/local/cargo/registry/cache | |
| /usr/local/cargo/registry/index | |
| target | |
| key: codspeed-v3-${{ runner.os }}-${{ runner.arch }}-${{ matrix.package }}-${{ steps.source.outputs.key }} | |
| restore-keys: codspeed-v3-${{ runner.os }}-${{ runner.arch }}-${{ matrix.package }}- | |
| # Cargo uses mtimes for freshness. Age files only when their blobs match the cached manifest, so | |
| # a pull request rebuilds changed sources while reusing every unaffected main-branch artifact. | |
| - name: Preserve cached benchmark build | |
| if: steps.benchmark-cache.outputs.cache-matched-key != '' | |
| env: | |
| CURRENT_KEY: codspeed-v3-${{ runner.os }}-${{ runner.arch }}-${{ matrix.package }}-${{ steps.source.outputs.key }} | |
| RESTORED_KEY: ${{ steps.benchmark-cache.outputs.cache-matched-key }} | |
| run: | | |
| if [[ -f target/codspeed-sources ]]; then | |
| comm -z -12 \ | |
| <(sort -z target/codspeed-sources) \ | |
| <(git ls-files -s -z -- .cargo crates Cargo.lock Cargo.toml rust-toolchain.toml | sort -z) \ | |
| | cut -z -f2 \ | |
| | xargs -0 -r touch --date=@0 -- | |
| elif [[ "$RESTORED_KEY" == "$CURRENT_KEY" ]]; then | |
| git ls-files -z -- .cargo crates Cargo.lock Cargo.toml rust-toolchain.toml \ | |
| | xargs -0 touch --date=@0 -- | |
| fi | |
| - run: ci/run-codspeed.sh ${{ matrix.package }} ${{ matrix.cargo_jobs }} | |
| env: | |
| GH_MATRIX: ${{ toJSON(matrix) }} | |
| GH_STRATEGY: ${{ toJSON(strategy) }} | |
| - name: Record benchmark sources | |
| if: github.ref == 'refs/heads/main' && steps.benchmark-cache.outputs.cache-hit != 'true' | |
| run: git ls-files -s -z -- .cargo crates Cargo.lock Cargo.toml rust-toolchain.toml > target/codspeed-sources | |
| - name: Save benchmark cache | |
| if: github.ref == 'refs/heads/main' && steps.benchmark-cache.outputs.cache-hit != 'true' | |
| uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: | | |
| /usr/local/cargo/git/db | |
| /usr/local/cargo/registry/cache | |
| /usr/local/cargo/registry/index | |
| target | |
| key: codspeed-v3-${{ runner.os }}-${{ runner.arch }}-${{ matrix.package }}-${{ steps.source.outputs.key }} | |
| analysis: | |
| name: CodSpeed analysis | |
| if: always() | |
| needs: [changes, baseline, operations] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| permissions: | |
| actions: read | |
| checks: read | |
| contents: read | |
| steps: | |
| - name: Verify the aggregate analysis | |
| env: | |
| ANALYSIS_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| EXPECT_ANALYSIS: >- | |
| ${{ github.event_name == 'pull_request' && | |
| needs.changes.outputs.bench_matrix != '[]' && | |
| needs.changes.outputs.workflow_changed != 'true' && | |
| needs.baseline.outputs.compatible == 'true' && | |
| needs.operations.result == 'success' }} | |
| GH_REPO: ${{ github.repository }} | |
| GH_TOKEN: ${{ github.token }} | |
| RUN_ID: ${{ github.run_id }} | |
| run: | | |
| if [[ "$EXPECT_ANALYSIS" != true ]]; then | |
| echo "No uploaded pull-request analysis is expected." | |
| exit 0 | |
| fi | |
| run_started_at=$(gh api "repos/$GH_REPO/actions/runs/$RUN_ID" --jq .created_at) | |
| for attempt in {1..60}; do | |
| check=$(gh api "repos/$GH_REPO/commits/$ANALYSIS_SHA/check-runs?per_page=100" \ | |
| | jq --arg started "$run_started_at" \ | |
| '[.check_runs[] | select(.name == "CodSpeed Performance Analysis" and .app.slug == "codspeed-hq" and .started_at >= $started)] | sort_by(.started_at) | last // {}') | |
| status=$(jq -r '.status // "missing"' <<< "$check") | |
| if [[ "$status" == completed ]]; then | |
| conclusion=$(jq -r '.conclusion' <<< "$check") | |
| echo "CodSpeed analysis concluded: $conclusion" | |
| [[ "$conclusion" == success ]] | |
| exit | |
| fi | |
| echo "Waiting for aggregate CodSpeed analysis ($status, attempt $attempt/60)." | |
| sleep 10 | |
| done | |
| echo "::warning::CodSpeed did not publish aggregate analysis within 10 minutes; benchmark uploads succeeded." | |
| echo "CodSpeed analysis unavailable" >> "$GITHUB_STEP_SUMMARY" | |
| # Required-status-check anchor: branch protection requires this job, not base/operations, because a | |
| # skipped required check blocks merge on GitHub. Passes when every dependency succeeded or skipped; | |
| # fails if any failed or was cancelled. | |
| codspeed-gate: | |
| if: always() | |
| needs: [changes, baseline, codspeed-image, operations, analysis] | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - name: Verify no required job failed | |
| env: | |
| RESULTS: ${{ join(needs.*.result, ' ') }} | |
| run: | | |
| echo "dependency results: $RESULTS" | |
| for result in $RESULTS; do | |
| case "$result" in | |
| failure|cancelled) echo "a required job reported: $result"; exit 1 ;; | |
| esac | |
| done | |
| echo "all required jobs passed or were skipped" |