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
# Only the latest branch or PR run is relevant under start-and-repair.
# Focused manual checks and nightly runs must not cancel or be cancelled by push runs.
concurrency:
group: ${{ github.workflow }}-${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') && github.run_id || github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
jj-performance-page:
name: jj performance page freshness
runs-on: ubuntu-26.04
timeout-minutes: 2
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- run: python3 scripts/jj-e2e/render-performance.py --check
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 # v7.0.1
with:
persist-credentials: false
- run: rustup toolchain install 1.97.1 --profile minimal
- name: Rust cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
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 }}, ${{ matrix.suite }})
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) }}
# Feature-set builds run beside the all-features tests so neither
# macOS job approaches the timeout. Each suite keeps its own cache.
suite: ${{ fromJSON(needs.changes.outputs.full == 'true' && '["main","features"]' || '["main"]') }}
runs-on: ${{ matrix.os }}
# Interim: the serial all-features test step can exceed 30 minutes on hosted macOS.
timeout-minutes: 45
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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 # v2.9.2
env:
RUSTUP_TOOLCHAIN: stable
with:
# main keeps the pre-split key so PR runs restore the existing warm cache.
shared-key: ${{ matrix.suite == 'main' && 'stable-tests' || 'stable-tests-features' }}
cache-on-failure: true
save-if: ${{ github.ref == 'refs/heads/main' }}
- run: cargo +stable clippy --locked --all-features --all-targets -- -D warnings
if: ${{ matrix.suite == 'main' }}
- run: cargo +stable clippy --locked -p girt-cli --all-targets -- -D warnings
if: ${{ matrix.suite == 'main' }}
- name: Rust formatting
if: ${{ matrix.suite == 'main' && runner.os == 'Linux' }}
run: |
rustup toolchain install nightly --profile minimal --component rustfmt
cargo +nightly fmt --all -- --check
# Tracing fixture dispatchers share tracing-core's global callsite cache.
- run: cargo +stable test --locked --all-features -- --test-threads=1
if: ${{ matrix.suite == 'main' }}
- run: cargo +stable test --locked -p girt-cli
if: ${{ matrix.suite == 'main' }}
- name: Developer guide examples
if: ${{ matrix.suite == 'main' }}
env:
RUSTUP_TOOLCHAIN: stable
run: python3 scripts/check-guide-examples.py
- name: Core-only runtime without tracing
if: ${{ matrix.suite == 'features' }}
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 short_ids --test sha256 --test commits --test builders --test repo --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
if: ${{ matrix.suite == 'features' }}
run: cargo +stable check --locked --lib --no-default-features
- name: HTTP-only library
if: ${{ matrix.suite == 'features' }}
run: cargo +stable check --locked --lib --no-default-features --features http
- name: Tracing without network dependencies
if: ${{ matrix.suite == 'features' }}
run: cargo +stable test --locked --no-default-features --features tracing --test tracing
- name: Tracing HTTP lifecycle
if: ${{ matrix.suite == 'features' }}
run: cargo +stable test --locked --no-default-features --features tracing,http --test tracing
- name: Tracing SSH-only library
if: ${{ matrix.suite == 'features' }}
run: cargo +stable check --locked --lib --no-default-features --features tracing,ssh
- name: SSH-only library
if: ${{ matrix.suite == 'features' }}
run: cargo +stable check --locked --lib --no-default-features --features ssh
- name: HTTP-only runtime without tracing
if: ${{ matrix.suite == 'features' }}
run: cargo +stable test --locked --no-default-features --features http --no-fail-fast --test http --test http_portable
- name: SSH-only runtime without tracing
if: ${{ matrix.suite == 'features' }}
run: cargo +stable test --locked --no-default-features --features ssh --test ssh
- run: cargo +stable run --locked --example fetch_local
if: ${{ matrix.suite == 'main' && needs.changes.outputs.full == 'true' }}
- run: cargo +stable run --locked --example push_local
if: ${{ matrix.suite == 'main' && needs.changes.outputs.full == 'true' }}
- run: cargo +stable run --locked --features http --example http_local
if: ${{ matrix.suite == 'main' && needs.changes.outputs.full == 'true' }}
- run: cargo +stable run --locked --features ssh --example ssh_local
if: ${{ matrix.suite == 'main' && needs.changes.outputs.full == 'true' }}
- run: cargo +stable run --locked --example content_diff
if: ${{ matrix.suite == 'main' && needs.changes.outputs.full == 'true' }}
- run: cargo +stable run --locked --example index
if: ${{ matrix.suite == 'main' && needs.changes.outputs.full == 'true' }}
- run: cargo +stable run --locked --example write_pack
if: ${{ matrix.suite == 'main' && needs.changes.outputs.full == 'true' }}
- run: cargo +stable run --locked --example init_repository
if: ${{ matrix.suite == 'main' && needs.changes.outputs.full == 'true' }}
- run: cargo +stable run --locked --features tracing --example tracing
if: ${{ matrix.suite == 'main' && needs.changes.outputs.full == 'true' }}
- run: cargo +stable run --locked --example reference_transaction
if: ${{ matrix.suite == 'main' && needs.changes.outputs.full == 'true' }}
- run: cargo +stable run --locked --example status
if: ${{ matrix.suite == 'main' && needs.changes.outputs.full == 'true' }}
- run: cargo +stable run --locked --example checkout
if: ${{ matrix.suite == 'main' && needs.changes.outputs.full == 'true' }}
- name: Rustdoc
if: ${{ matrix.suite == 'main' && needs.changes.outputs.full == 'true' }}
env:
RUSTDOCFLAGS: -D warnings
run: cargo +stable doc --locked --all-features --no-deps --document-private-items
- name: docs.rs build
if: ${{ matrix.suite == 'main' && runner.os == 'Linux' && needs.changes.outputs.full == 'true' }}
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]
# Divide the existing checks by feature configuration to avoid duplicate builds.
# Portable integrations are explicit; native local transfer runs on Windows.
runs-on: windows-2022
# Interim: the all-features suite can exceed 20 minutes on hosted Windows.
timeout-minutes: 45
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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 # v2.9.2
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 # v2
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 short_ids --test sha256 --test blobs --test trees --test commits --test builders --test repo --test decoding --test discovery --test tags --test packs --test pack_index_rebuild --test multi_pack_index --test bitmaps
--test signing --test signing_gpg --test history --test commit_graph --test repositories --test layout_shallow --test worktree_create --test worktree_admin --test config_resolution --test config_edit --test submodules --test submodule_plans --test submodule_execute --test submodule_populate --test submodule_recursive --test remotes --test tree_compare --test content_diff --test diffstat --test patch --test patch_binary --test format_patch --test format_patch_series --test am_recovery --test am_unborn --test am_commit --test am_apply --test am_state --test mailbox --test mailinfo --test mailinfo_mime --test apply --test apply_tree --test apply_extended --test apply_index --test apply_worktree --test blame --test index --test colocation --test status_portable --test checkout_portable --test checkout_gitlinks --test references --test references_portable --test reflog_expiry --test retention --test lock_cleanup --test maintenance_run
--test maintenance_loose_objects --test maintenance_incremental_repack
--test loose_pack_names
--test pack_loose
--test incremental_repack_write
--test refspec_mapping_control
--test local_advertisement_control
--test fetch_finish_control
--test prune_packed_open --test repository_open_control
--test files_transaction_control
--test prune_packed_prefix
- 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 # v7.0.1
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 # v2.9.2
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 # v7.0.1
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 # v2.9.2
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 # v7.0.1
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 # v2.9.2
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 # v7.0.1
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 # v7.0.1
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 # v2.9.2
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 # v7.0.1
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 # v7.0.1
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 # v2.9.2
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 }}
guides: ${{ steps.paths.outputs.guides }}
index: ${{ steps.paths.outputs.index }}
references: ${{ steps.paths.outputs.references }}
security: ${{ steps.paths.outputs.security }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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 guides=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 guides=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='(\.mdx?$|^docs/|^LICENSE|^\.config/vale\.ini$|^\.config/rumdl\.toml$|^\.config/vale/)'
# The website has its own workflow (site.yml) and never affects the Rust checks.
site='(^site/|^\.github/workflows/site\.yml$|^\.gitattributes$)'
if [ -n "${all:-}" ] || grep -vE "$site" <<<"$changed" | grep -qvE "$prose"; 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 guides '^site/src/content/docs/guides/'
route security '^(\.github/|Cargo\.(toml|lock)$|\.config/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 # v7.0.1
with:
persist-credentials: false
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
- 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
# Rust changes check these programs in the Unix job. Guide-only edits still need execution.
- name: Guide example toolchain
if: ${{ needs.changes.outputs.guides == 'true' && needs.changes.outputs.rust != 'true' }}
run: rustup toolchain install stable --profile minimal
- name: Developer guide examples
env:
RUSTUP_TOOLCHAIN: stable
if: ${{ needs.changes.outputs.guides == 'true' && needs.changes.outputs.rust != 'true' }}
run: python3 scripts/check-guide-examples.py
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:
# The trusted semver-label workflow matches this job name.
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 # v7.0.1
with:
persist-credentials: false
# Keep the merge commit and both parents available for --baseline-rev.
fetch-depth: 2
- name: Toolchain
run: rustup toolchain install stable --profile minimal
- name: Rust cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
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' }}
# Match Changes: use the actual merge parent, even if main advanced since the event.
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"
# The trusted workflow_run labeler also uses this named step for fork PRs.
- 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:
# No checkout or PR code runs with the label-writing token.
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
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}`);
}
# Aggregate evidence for every job. Admin readiness follows CONTRIBUTING.md.
required:
name: required
if: ${{ always() }}
needs:
- jj-performance-page
- 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
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