name: CI
on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
schedule:
- cron: "23 8 * * *"
workflow_dispatch:
inputs:
ignore_windows_only:
description: Restrict focused ignore checks to Windows
type: boolean
default: false
ignore_only:
description: Run focused ignore compatibility and performance checks
type: boolean
default: false
storage_only:
description: Run only focused file-backed storage checks
type: boolean
default: false
alternates_only:
description: Run only focused native alternate storage checks
type: boolean
default: false
http_only:
description: Run only focused Windows HTTP regression checks
type: boolean
default: false
retention_only:
description: Run focused retention and repack checks
type: boolean
default: false
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event.pull_request.head.sha || github.sha }}
cancel-in-progress: false
queue: max
jobs:
msrv:
name: MSRV (1.97.1)
needs: changes
if: ${{ needs.changes.outputs.rust == 'true' }}
runs-on: ubuntu-26.04
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with:
persist-credentials: false
- run: rustup toolchain install 1.97.1 --profile minimal
- name: Rust cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 env:
RUSTUP_TOOLCHAIN: 1.97.1
with:
shared-key: msrv
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/main' }}
- run: cargo +1.97.1 check --locked --all-targets --all-features
- run: cargo +1.97.1 test --locked --no-default-features --lib
unix:
name: Test (${{ matrix.os }})
needs: changes
if: ${{ needs.changes.outputs.rust == 'true' && !inputs.ignore_only && !inputs.http_only && !inputs.alternates_only && !inputs.storage_only && !inputs.retention_only }}
strategy:
fail-fast: false
matrix:
os: ${{ fromJSON(needs.changes.outputs.unix) }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with:
persist-credentials: false
- name: Toolchain and environment
run: |
rustup toolchain install stable --profile minimal --component clippy
rustc +stable -Vv
git --version
uname -a
if [ "$RUNNER_OS" = Linux ]; then
cat /etc/os-release
df -T . "$(python3 -c 'import tempfile; print(tempfile.gettempdir())')"
else
sw_vers
diskutil info "$(df . | tail -1 | awk '{print $1}')"
diskutil info "$(df "$(python3 -c 'import tempfile; print(tempfile.gettempdir())')" | tail -1 | awk '{print $1}')"
fi
- name: Rust cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 env:
RUSTUP_TOOLCHAIN: stable
with:
shared-key: stable-tests
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/main' }}
- run: cargo +stable test --locked --all-features -- --test-threads=1
- name: Core-only runtime without tracing
run: >-
cargo +stable test --locked --no-default-features --no-fail-fast
--lib --test layout_shallow --test worktree_create --test worktree_admin --test object_ids --test sha256 --test commits --test decoding --test packs --test index --test colocation --test references --test references_portable --test reflog_expiry --test retention --test retention_noop --test retention_integrity --test retention_survivors --test retention_repack_contracts
- name: Core-only library
run: cargo +stable check --locked --lib --no-default-features
- name: HTTP-only library
run: cargo +stable check --locked --lib --no-default-features --features http
- name: Tracing without network dependencies
run: cargo +stable test --locked --no-default-features --features tracing --test tracing
- name: Tracing HTTP lifecycle
run: cargo +stable test --locked --no-default-features --features tracing,http --test tracing
- name: Tracing SSH-only library
run: cargo +stable check --locked --lib --no-default-features --features tracing,ssh
- name: SSH-only library
run: cargo +stable check --locked --lib --no-default-features --features ssh
- name: HTTP-only runtime without tracing
run: cargo +stable test --locked --no-default-features --features http --no-fail-fast --test http --test http_portable
- name: SSH-only runtime without tracing
run: cargo +stable test --locked --no-default-features --features ssh --test ssh
- run: cargo +stable clippy --locked --all-features --all-targets -- -D warnings
- run: cargo +stable run --locked --example fetch_local
- run: cargo +stable run --locked --example push_local
- run: cargo +stable run --locked --features http --example http_local
- run: cargo +stable run --locked --features ssh --example ssh_local
- run: cargo +stable run --locked --example content_diff
- run: cargo +stable run --locked --example index
- run: cargo +stable run --locked --example write_pack
- run: cargo +stable run --locked --example init_repository
- run: cargo +stable run --locked --features tracing --example tracing
- run: cargo +stable run --locked --example reference_transaction
- run: cargo +stable run --locked --example status
- run: cargo +stable run --locked --example checkout
- name: Rustdoc
env:
RUSTDOCFLAGS: -D warnings
run: cargo +stable doc --locked --all-features --no-deps --document-private-items
- name: Rust formatting
if: runner.os == 'Linux'
run: |
rustup toolchain install nightly --profile minimal --component rustfmt
cargo +nightly fmt --all -- --check
- name: docs.rs build
if: runner.os == 'Linux'
env:
RUSTDOCFLAGS: -D warnings
run: |
cargo +stable install cargo-docs-rs --locked --version 1.0.4
cargo +nightly docs-rs
windows:
name: Test (windows-2022, ${{ matrix.suite }})
needs: changes
if: ${{ needs.changes.outputs.rust == 'true' && needs.changes.outputs.full == 'true' && !inputs.ignore_only && !inputs.http_only && !inputs.alternates_only && !inputs.storage_only && !inputs.retention_only }}
strategy:
fail-fast: false
matrix:
suite: [all-features, core, integrations]
runs-on: windows-2022
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with:
persist-credentials: false
- name: Toolchain and environment
run: |
rustup toolchain install stable --profile minimal --component clippy
rustc +stable -Vv
git --version
Get-CimInstance Win32_OperatingSystem | Select-Object Caption, Version, BuildNumber | ConvertTo-Json -Compress
Get-Volume | Where-Object DriveLetter | Select-Object DriveLetter, FileSystem, FileSystemLabel | ConvertTo-Json -Compress
Write-Output "Temporary directory: $env:TEMP"
- name: Rust cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 env:
RUSTUP_TOOLCHAIN: stable
with:
shared-key: stable-tests-${{ matrix.suite }}
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
uses: taiki-e/install-action@1168f6810a3cb30a3514bf5756a3b7f0422c6df8 with:
tool: cargo-nextest@0.9.140
- name: Native local transfer
if: ${{ matrix.suite == 'integrations' }}
run: cargo +stable nextest run --profile windows-ci --locked --no-default-features --test local_native --test transfer_reuse --test push_deltas --test local_transfer_progress
- name: Core-only library
if: ${{ matrix.suite == 'core' }}
run: cargo +stable check --locked --lib --no-default-features
- name: HTTP-only library
if: ${{ matrix.suite == 'core' }}
run: cargo +stable check --locked --lib --no-default-features --features http
- name: Tracing without network dependencies
if: ${{ matrix.suite == 'core' }}
run: cargo +stable nextest run --profile windows-ci --locked --no-default-features --features tracing --test tracing
- name: Tracing HTTP lifecycle
if: ${{ matrix.suite == 'core' }}
run: cargo +stable nextest run --profile windows-ci --locked --no-default-features --features tracing,http --test tracing
- name: Tracing SSH-only library
if: ${{ matrix.suite == 'core' }}
run: cargo +stable check --locked --lib --no-default-features --features tracing,ssh
- name: SSH-only library
if: ${{ matrix.suite == 'core' }}
run: cargo +stable check --locked --lib --no-default-features --features ssh
- run: cargo +stable clippy --locked --all-features --all-targets -- -D warnings
if: ${{ matrix.suite == 'all-features' }}
- run: cargo +stable nextest run --profile windows-ci --locked --all-features --lib
if: ${{ matrix.suite == 'all-features' }}
- name: Core-only units without tracing
if: ${{ matrix.suite == 'core' }}
run: cargo +stable nextest run --profile windows-ci --locked --no-default-features --lib
- run: cargo +stable test --locked --all-features --doc
if: ${{ matrix.suite == 'all-features' }}
- name: Portable Git interoperability in both object formats
if: ${{ matrix.suite == 'integrations' }}
run: >-
cargo +stable nextest run --profile windows-ci --locked --no-default-features --no-fail-fast
--test object_ids --test sha256 --test blobs --test trees --test commits --test decoding --test discovery --test tags --test packs --test pack_index_rebuild
--test history --test repositories --test layout_shallow --test worktree_create --test worktree_admin --test config_resolution --test config_edit --test remotes --test tree_compare --test content_diff --test index --test colocation --test status_portable --test checkout_portable --test references --test references_portable --test reflog_expiry --test retention
- name: Portable HTTP runtime
if: ${{ !cancelled() && matrix.suite == 'integrations' }}
run: cargo +stable nextest run --profile windows-ci --locked --no-default-features --features http --no-fail-fast --test http --test http_portable
- run: cargo +stable run --locked --example content_diff
if: ${{ matrix.suite == 'all-features' }}
- run: cargo +stable run --locked --example index
if: ${{ matrix.suite == 'all-features' }}
- run: cargo +stable run --locked --example write_pack
if: ${{ matrix.suite == 'all-features' }}
- run: cargo +stable run --locked --example init_repository
if: ${{ matrix.suite == 'all-features' }}
- run: cargo +stable run --locked --features tracing --example tracing
if: ${{ matrix.suite == 'all-features' }}
- name: Public Rustdoc
if: ${{ matrix.suite == 'all-features' }}
env:
RUSTDOCFLAGS: -D warnings
run: cargo +stable doc --locked --all-features --no-deps
windows-http:
name: HTTPS (windows-2022)
if: ${{ inputs.http_only }}
runs-on: windows-2022
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with:
persist-credentials: false
- name: Toolchain and environment
run: |
rustup toolchain install stable --profile minimal --component clippy
rustc +stable -Vv
git --version
Get-CimInstance Win32_OperatingSystem | Select-Object Caption, Version, BuildNumber | ConvertTo-Json -Compress
Get-Volume | Where-Object DriveLetter | Select-Object DriveLetter, FileSystem, FileSystemLabel | ConvertTo-Json -Compress
Write-Output "Temporary directory: $env:TEMP"
- name: Rust cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 env:
RUSTUP_TOOLCHAIN: stable
with:
shared-key: stable-area
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: HTTP runtime regression
run: cargo +stable test --locked --no-default-features --features http --no-fail-fast --test http --test http_portable -- --show-output
- name: HTTP test Clippy
run: cargo +stable clippy --locked --no-default-features --features http --test http --test http_portable -- -D warnings
alternates:
name: Alternates (${{ matrix.os }})
if: ${{ inputs.alternates_only }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-26.04, macos-14, windows-2022]
runs-on: ${{ matrix.os }}
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with:
persist-credentials: false
- name: Toolchain and environment
run: |
rustup toolchain install stable --profile minimal --component clippy
rustc +stable -Vv
git --version
python -c "import platform,tempfile; print(platform.platform()); print(tempfile.gettempdir())"
- name: Rust cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 env:
RUSTUP_TOOLCHAIN: stable
with:
shared-key: stable-area
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Both-format alternate and repository contracts
run: cargo +stable test --locked --no-default-features --test alternates --test repositories --test packs
- name: Parser units and tracing build
run: cargo +stable test --locked --all-features --lib objects::alternates
- name: Scoped Clippy
run: cargo +stable clippy --locked --all-features --lib --test alternates --test repositories --test packs -- -D warnings
- name: Original Git observations
run: python tests/fixtures/alternates/observe.py
storage:
name: Storage (${{ matrix.os }})
if: ${{ inputs.storage_only }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-26.04, macos-14, windows-2022]
runs-on: ${{ matrix.os }}
timeout-minutes: 25
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with:
persist-credentials: false
- name: Environment
run: |
rustup toolchain install stable --profile minimal --component clippy
rustc +stable -Vv
git --version
python -c "import platform,tempfile; print(platform.platform()); print(tempfile.gettempdir())"
- name: Rust cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 env:
RUSTUP_TOOLCHAIN: stable
with:
shared-key: stable-area
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Storage behavior and original decoder corpus
run: cargo +stable test --locked --all-features --lib --test packs --test alternates --test repositories --test refresh
- name: Scoped Clippy
run: cargo +stable clippy --locked --all-features --lib --test packs --test alternates --test repositories --test refresh --example storage_resources --bench packs -- -D warnings
- name: Refresh after received-pack installation
if: runner.os != 'Windows'
run: cargo +stable test --locked --all-features --test fetch imports_server_deltas_and_all_object_kinds
- name: Large original Git fixture
run: python tests/fixtures/file-storage/generate.py large-store sha1
- name: Build isolated resource probe
run: cargo +stable build --locked --release --example storage_resources
- name: Measure reader resources and cancellation
run: python tests/fixtures/file-storage/measure.py large-store
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a if: always()
with:
name: storage-${{ matrix.os }}
path: |
large-store/workload.json
storage-measurements.json
ignore:
name: Ignore (${{ matrix.os }})
if: ${{ inputs.ignore_only }}
strategy:
fail-fast: false
matrix:
os: ${{ inputs.ignore_windows_only && fromJSON('["windows-2022"]') || fromJSON('["ubuntu-26.04", "macos-14", "windows-2022"]') }}
runs-on: ${{ matrix.os }}
timeout-minutes: 25
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with:
persist-credentials: false
- name: Environment
run: |
rustup toolchain install stable --profile minimal --component clippy
rustc +stable -Vv
git --version
python -c "import platform,tempfile; print(platform.platform()); print(tempfile.gettempdir())"
- name: Rust cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 env:
RUSTUP_TOOLCHAIN: stable
with:
shared-key: stable-area
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Unit and public compatibility cases
run: "cargo +stable test --locked --no-default-features --lib --test ignore ignore::"
- name: Both-format Git comparisons
run: cargo +stable test --locked --no-default-features --test ignore
- name: Tracing feature composition
run: "cargo +stable test --locked --no-default-features --features tracing --lib --test ignore ignore::"
- name: Clippy
run: cargo +stable clippy --locked --no-default-features --lib --test ignore --bench ignore --example ignore_batch -- -D warnings
- name: Build batch adapter
run: cargo +stable build --locked --release --example ignore_batch
- name: Independent adversarial observations
run: python tests/fixtures/ignore/observe.py > ignore-observations.json
- name: Large source resources
run: python tests/fixtures/ignore/resources.py > ignore-resources.json
- name: Representative matched batches
run: cargo +stable bench --locked --bench ignore -- --noplot
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a if: always()
with:
name: ignore-${{ matrix.os }}
path: |
ignore-observations.json
ignore-resources.json
target/criterion
retention:
name: Retention (${{ matrix.os }})
if: ${{ inputs.retention_only }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-26.04, macos-14, windows-2022]
runs-on: ${{ matrix.os }}
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with:
persist-credentials: false
- name: Toolchain and Git
run: |
rustup toolchain install stable --profile minimal --component clippy
rustc +stable -Vv
git --version
python -c "import platform,tempfile; print(platform.platform()); print(tempfile.gettempdir())"
- name: Rust cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 env:
RUSTUP_TOOLCHAIN: stable
with:
shared-key: stable-area
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Retention and repack cases
run: cargo +stable test --locked --no-default-features --test reflog_expiry --test retention --test retention_noop --test retention_integrity --test retention_survivors --test retention_repack_contracts
- name: Maintenance fault and race cases
run: |
cargo +stable test --locked --no-default-features --lib 'retention::'
cargo +stable test --locked --no-default-features --lib 'refs::reftable::stack_tests::expiry_'
- name: Retention API and benchmark lint
run: cargo +stable clippy --locked --no-default-features --lib --test reflog_expiry --test retention --test retention_noop --test retention_integrity --test retention_survivors --test retention_repack_contracts --bench retention -- -D warnings
changes:
name: Changes
runs-on: ubuntu-26.04
timeout-minutes: 5
outputs:
full: ${{ steps.scope.outputs.full }}
unix: ${{ steps.scope.outputs.unix }}
rust: ${{ steps.paths.outputs.rust }}
docs: ${{ steps.paths.outputs.docs }}
index: ${{ steps.paths.outputs.index }}
references: ${{ steps.paths.outputs.references }}
security: ${{ steps.paths.outputs.security }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with:
fetch-depth: 0
filter: blob:none
persist-credentials: false
- name: Select platform scope
id: scope
env:
FULL: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'full-ci') || startsWith(github.head_ref, 'release-plz-') }}
run: |
echo "full=$FULL" >> "$GITHUB_OUTPUT"
if [ "$FULL" = true ]; then
echo 'unix=["ubuntu-26.04","macos-14"]' >> "$GITHUB_OUTPUT"
else
echo 'unix=["ubuntu-26.04"]' >> "$GITHUB_OUTPUT"
fi
- name: Select checks for changed paths
id: paths
env:
EVENT: ${{ github.event_name }}
run: |
# Every publishable main revision needs full evidence, including docs-only pushes.
if [ "$EVENT" = schedule ] || [ "$EVENT" = push ]; then
printf '%s\n' rust=true docs=true index=true references=true security=true >> "$GITHUB_OUTPUT"
exit 0
fi
# A manual run keeps the Rust jobs, whose inputs select focused checks, and skips the rest.
if [ "$EVENT" = workflow_dispatch ]; then
printf '%s\n' rust=true docs=false index=false references=false security=false >> "$GITHUB_OUTPUT"
exit 0
fi
# A pull request checks out its merge commit, whose first parent is the base branch.
if [ "$EVENT" = pull_request ]; then
changed=$(git diff --name-only HEAD^1 HEAD)
else
changed=
all=true
fi
printf '%s\n' "$changed"
# Documentation-only files, including everything under docs/.
prose='(\.md$|^docs/|^LICENSE|^\.vale\.ini$|^\.config/rumdl\.toml$|^\.config/vale/)'
if [ -n "${all:-}" ] || grep -qvE "$prose" <<<"$changed"; then
echo rust=true >> "$GITHUB_OUTPUT"
else
echo rust=false >> "$GITHUB_OUTPUT"
fi
route() {
if [ -n "${all:-}" ] || grep -qE "$2" <<<"$changed"; then
echo "$1=true" >> "$GITHUB_OUTPUT"
else
echo "$1=false" >> "$GITHUB_OUTPUT"
fi
}
route index '^(src/|tests/(index|colocation)\.rs$|\.github/workflows/(index|validation)\.yml$)'
route references '^(src/|tests/(references_portable|worktree_create|worktree_admin)\.rs$|\.github/workflows/(references|validation)\.yml$)'
route docs "$prose|^(justfile|\.github/workflows/validation\.yml)\$"
route security '^(\.github/|Cargo\.(toml|lock)$|deny\.toml$)'
docs:
name: Docs
needs: changes
if: ${{ needs.changes.outputs.docs == 'true' }}
runs-on: ubuntu-26.04
timeout-minutes: 10
env:
VALE_VERSION: 3.24.0
VALE_SHA256: 867534ddb678abca7f214bc4706ead0922bd5252ffe8035fc227013f66c8b214
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with:
persist-credentials: false
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 - name: Install just and rumdl
run: uv tool install "rust-just==1.58.0" && uv tool install "rumdl==0.2.77"
- name: Install Vale
run: |
archive="vale_${VALE_VERSION}_Linux_64-bit.tar.gz"
curl -fsSLO "https://github.com/errata-ai/vale/releases/download/v${VALE_VERSION}/${archive}"
echo "${VALE_SHA256} ${archive}" | sha256sum -c -
mkdir -p "$HOME/.local/bin"
tar -xzf "$archive" -C "$HOME/.local/bin" vale
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Markdown format and prose
run: just docs-check
index:
name: Index
needs: changes
if: ${{ needs.changes.outputs.index == 'true' }}
with:
full: ${{ needs.changes.outputs.full == 'true' }}
uses: $/.github/workflows/index.yml
references:
name: References
needs: changes
if: ${{ needs.changes.outputs.references == 'true' }}
with:
full: ${{ needs.changes.outputs.full == 'true' }}
uses: $/.github/workflows/references.yml
security:
name: Security
needs: changes
if: ${{ needs.changes.outputs.security == 'true' }}
uses: $/.github/workflows/security.yml
semver:
name: Semver
needs: changes
if: ${{ needs.changes.outputs.rust == 'true' && (github.event_name == 'pull_request' || github.event_name == 'push') }}
runs-on: ubuntu-26.04
timeout-minutes: 15
outputs:
breaking: ${{ steps.result.outputs.breaking }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with:
persist-credentials: false
fetch-depth: 2
- name: Toolchain
run: rustup toolchain install stable --profile minimal
- name: Rust cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 env:
RUSTUP_TOOLCHAIN: stable
with:
shared-key: semver
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install cargo-semver-checks
env:
SEMVER_SHA256: cacadafaa5cd27ae6d05a74a103266917ebab4c5e43c898360e177d4fbf06a65
run: |
curl -fsSL --retry 3 -o "$RUNNER_TEMP/semver.tar.gz" https://github.com/obi1kenobi/cargo-semver-checks/releases/download/v0.51.0/cargo-semver-checks-x86_64-unknown-linux-gnu.tar.gz
echo "$SEMVER_SHA256 $RUNNER_TEMP/semver.tar.gz" | sha256sum -c -
tar -xzf "$RUNNER_TEMP/semver.tar.gz" -C "$HOME/.cargo/bin" cargo-semver-checks
- name: Latest published release
id: baseline
run: |
version=$(curl -fsSL --retry 3 -A 'girt-semver-ci (https://github.com/joshka/girt)' https://crates.io/api/v1/crates/girt | jq -er '[.versions[] | select(.yanked == false and (.num | contains("-") | not)) | .num] | sort_by(split(".") | map(tonumber)) | last')
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "Baseline: girt $version"
- name: Compare public API
id: compare
env:
BASELINE: ${{ steps.baseline.outputs.version }}
run: |
status=0
cargo +stable semver-checks -p girt --baseline-version "$BASELINE" --release-type minor --all-features || status=$?
case "$status" in
0) echo breaking=false >> "$GITHUB_OUTPUT" ;;
100) echo breaking=true >> "$GITHUB_OUTPUT" ;;
*) echo "cargo-semver-checks failed to complete (exit $status)" >&2; exit "$status" ;;
esac
- name: Compare PR base
id: base-compare
if: ${{ github.event_name == 'pull_request' && steps.compare.outputs.breaking == 'true' }}
run: |
status=0
cargo +stable semver-checks -p girt --baseline-rev HEAD^1 --release-type minor --all-features || status=$?
case "$status" in
0) echo breaking=false >> "$GITHUB_OUTPUT" ;;
100) echo breaking=true >> "$GITHUB_OUTPUT" ;;
*) echo "PR-base comparison failed to complete (exit $status)" >&2; exit "$status" ;;
esac
- name: Select PR compatibility result
id: result
env:
EVENT: ${{ github.event_name }}
PUBLISHED_BREAK: ${{ steps.compare.outputs.breaking }}
BASE_BREAK: ${{ steps.base-compare.outputs.breaking }}
run: |
breaking="$PUBLISHED_BREAK"
if [ "$EVENT" = pull_request ] && [ "$PUBLISHED_BREAK" = true ]; then
breaking="${BASE_BREAK:?base comparison result missing}"
if [ "$breaking" = false ]; then
echo 'Only previously merged release breaks remain; this PR adds no break'
fi
fi
case "$breaking" in
true|false) ;;
*) echo 'Compatibility result missing or invalid' >&2; exit 1 ;;
esac
echo "breaking=$breaking" >> "$GITHUB_OUTPUT"
- name: Record breaking change
if: ${{ steps.result.outputs.breaking == 'true' }}
run: echo 'New published API break detected'
semver-policy:
name: Semver policy
needs: semver
if: ${{ github.event_name == 'pull_request' && needs.semver.outputs.breaking == 'true' }}
runs-on: ubuntu-26.04
timeout-minutes: 5
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd with:
script: |
const {owner, repo} = context.repo;
const issue_number = context.payload.pull_request.number;
const {data: pr} = await github.rest.pulls.get({owner, repo, pull_number: issue_number});
if (pr.head.sha !== context.payload.pull_request.head.sha) {
core.setFailed('PR head changed; wait for its current Semver check');
return;
}
if (pr.head.repo?.full_name === `${owner}/${repo}`) {
try {
await github.rest.issues.addLabels({owner, repo, issue_number, labels: ['breaking']});
} catch (error) {
if (error.status !== 403) throw error;
core.warning('Read-only bot token: the trusted workflow_run job will add breaking');
}
}
if (!pr.labels.some(label => label.name === 'next-breaking')) {
core.setFailed(`API breaks require next-breaking; batch them deliberately, then rerun this job. See Semver logs: ${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`);
}
required:
name: required
if: ${{ always() }}
needs:
- msrv
- unix
- windows
- windows-http
- alternates
- storage
- ignore
- retention
- changes
- docs
- index
- references
- security
- semver
- semver-policy
runs-on: ubuntu-26.04
timeout-minutes: 5
permissions:
contents: read
issues: write
steps:
- name: Require success or skip from every needed job
env:
NEEDS: ${{ toJSON(needs) }}
run: |
jq -r 'to_entries[] | "\(.key): \(.value.result)"' <<<"$NEEDS"
jq -e 'all(.[]; .result == "success" or .result == "skipped")' <<<"$NEEDS" > /dev/null
- name: Report main or nightly regression
if: ${{ failure() && (github.event_name == 'push' || github.event_name == 'schedule') }}
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd env:
NEEDS: ${{ toJSON(needs) }}
with:
script: |
const {owner, repo} = context.repo;
const title = 'CI regression on main';
const failed = Object.entries(JSON.parse(process.env.NEEDS))
.filter(([, job]) => !['success', 'skipped'].includes(job.result))
.map(([name, job]) => `${name}: ${job.result}`).join('\n');
const body = `Full main/nightly CI failed at ${context.sha}.\n\n${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}\n\n${failed}\n\nInvestigate before releasing; use full-ci on the fix PR.`;
const issues = await github.paginate(github.rest.issues.listForRepo,
{owner, repo, state: 'open', per_page: 100});
const existing = issues.find(issue => !issue.pull_request && issue.title === title &&
issue.user?.login === 'github-actions[bot]');
if (existing) {
await github.rest.issues.createComment({owner, repo, issue_number: existing.number, body});
} else {
await github.rest.issues.create({owner, repo, title, body});
}