Skip to content

chore(deps): bump actions/setup-go from 5 to 6 #8194

chore(deps): bump actions/setup-go from 5 to 6

chore(deps): bump actions/setup-go from 5 to 6 #8194

Workflow file for this run

name: CI
on:
merge_group:
push:
branches: [main]
pull_request:
workflow_dispatch:
# Cancel in-progress runs for PRs, queue for merge group and main
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}-v2
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
# Single source of truth for supported Python versions
SUPPORTED_PYTHONS: '["3.10", "3.11", "3.12", "3.13"]'
# Default Python version for non-matrix jobs
PYTHON_VERSION: "3.13"
# Minimum supported Python — used for ABI3 wheel builds and glob patterns.
# Must match the lowest entry in SUPPORTED_PYTHONS.
MINIMUM_PYTHON: "3.10"
# Number of runners to shard integration tests across (per runtime)
# Slow tests ([short] skip) are distributed round-robin first, then fast tests fill in
NUM_IT_RUNNER_SHARDS: "4"
# Standard environment
HYPOTHESIS_PROFILE: ci
FORCE_COLOR: "1"
PIP_DISABLE_PIP_VERSION_CHECK: "1"
PIP_NO_PYTHON_VERSION_WARNING: "1"
CARGO_TERM_COLOR: always
# CGo required for go-tree-sitter (static Python schema parser)
CGO_ENABLED: "1"
# Disable tools in mise that CI installs via dedicated GitHub Actions for
# better reliability (avoids transient GitHub Releases 502s from aqua downloads),
# better caching, and guaranteed tool ordering.
# - Rust toolchain: dtolnay/rust-toolchain
# - cargo-binstall: taiki-e/install-action
# - Python: astral-sh/setup-uv
# - golangci-lint: golangci/golangci-lint-action
# - gotestsum: go install (uses Go module proxy, not GitHub Releases)
# - cargo-deny, cargo-nextest: taiki-e/install-action
# - zig, cargo-zigbuild, maturin, cargo-insta: not needed in CI (maturin-action bundles zig)
MISE_DISABLE_TOOLS: rust,rustup,rustup-init,cargo-binstall,python,golangci-lint,gotestsum,cargo-deny,cargo-insta,cargo-nextest,cargo:cargo-nextest,zig,cargo-zigbuild,maturin,cargo:maturin
permissions: {}
# =============================================================================
# Change Detection
# =============================================================================
jobs:
changes:
name: Detect changes
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
go: ${{ steps.filter.outputs.go }}
rust: ${{ steps.filter.outputs.rust }}
python: ${{ steps.filter.outputs.python }}
integration: ${{ steps.filter.outputs.integration }}
docs: ${{ steps.filter.outputs.docs }}
version_only: ${{ steps.filter.outputs.version_only }}
version_changed: ${{ steps.filter.outputs.version_changed }}
# Pass through for matrix jobs (env context unavailable in strategy)
supported_pythons: ${{ env.SUPPORTED_PYTHONS }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Detect changed paths
id: filter
run: |
# For PRs, compare against base; for pushes, compare against previous commit
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE="${{ github.event.pull_request.base.sha }}"
else
BASE="${{ github.event.before }}"
# Handle initial push (no before)
if [ "$BASE" = "0000000000000000000000000000000000000000" ]; then
BASE="HEAD~1"
fi
fi
echo "Comparing $BASE..HEAD"
# Get changed files
CHANGED=$(git diff --name-only "$BASE" HEAD 2>/dev/null || echo "")
# Check if coglet version changed
VERSION_CHANGED="false"
if echo "$CHANGED" | grep -qE '^crates/Cargo\.toml$'; then
# Check if only the version line changed
if git diff "$BASE" HEAD -- crates/Cargo.toml | grep -qE '^\+version = '; then
VERSION_CHANGED="true"
echo "Coglet version changed"
fi
fi
echo "version_changed=$VERSION_CHANGED" >> $GITHUB_OUTPUT
# Check if ONLY the version changed (version bump PR)
# This is true if crates/Cargo.toml is the only file and only version line changed
VERSION_ONLY="false"
if [ "$VERSION_CHANGED" = "true" ]; then
FILE_COUNT=$(echo "$CHANGED" | grep -c . || echo "0")
if [ "$FILE_COUNT" = "1" ]; then
# Only crates/Cargo.toml changed, check if only version line changed
# Get actual diff lines (excluding +++ and --- headers)
DIFF_CONTENT=$(git diff "$BASE" HEAD -- crates/Cargo.toml | grep -E '^[+-]' | grep -v '^[+-]{3}')
# Should be exactly: -version = "old" and +version = "new"
MINUS_LINES=$(echo "$DIFF_CONTENT" | grep -c '^-' || echo "0")
PLUS_LINES=$(echo "$DIFF_CONTENT" | grep -c '^\+' || echo "0")
VERSION_MINUS=$(echo "$DIFF_CONTENT" | grep -c '^-version = ' || echo "0")
VERSION_PLUS=$(echo "$DIFF_CONTENT" | grep -c '^\+version = ' || echo "0")
if [ "$MINUS_LINES" = "1" ] && [ "$PLUS_LINES" = "1" ] && \
[ "$VERSION_MINUS" = "1" ] && [ "$VERSION_PLUS" = "1" ]; then
VERSION_ONLY="true"
echo "Version-only change detected - skipping heavy CI"
fi
fi
fi
echo "version_only=$VERSION_ONLY" >> $GITHUB_OUTPUT
# CI/tooling changes should run everything (unless version-only)
if [ "$VERSION_ONLY" = "true" ]; then
echo "go=false" >> $GITHUB_OUTPUT
echo "rust=false" >> $GITHUB_OUTPUT
echo "python=false" >> $GITHUB_OUTPUT
echo "integration=false" >> $GITHUB_OUTPUT
echo "docs=false" >> $GITHUB_OUTPUT
elif echo "$CHANGED" | grep -qE '^(\.github/workflows/|mise\.toml)'; then
echo "CI/tooling changed - running all jobs"
echo "go=true" >> $GITHUB_OUTPUT
echo "rust=true" >> $GITHUB_OUTPUT
echo "python=true" >> $GITHUB_OUTPUT
echo "integration=true" >> $GITHUB_OUTPUT
echo "docs=true" >> $GITHUB_OUTPUT
else
# Detect Go changes
if echo "$CHANGED" | grep -qE '^(cmd/|pkg/|go\.(mod|sum)|\.golangci\.yml|Makefile)'; then
echo "go=true" >> $GITHUB_OUTPUT
else
echo "go=false" >> $GITHUB_OUTPUT
fi
# Detect Rust changes
if echo "$CHANGED" | grep -qE '^(crates/|Cargo\.(toml|lock))'; then
echo "rust=true" >> $GITHUB_OUTPUT
else
echo "rust=false" >> $GITHUB_OUTPUT
fi
# Detect Python changes
if echo "$CHANGED" | grep -qE '^(python/|pyproject\.toml|uv\.lock|noxfile\.py|\.ruff\.toml)'; then
echo "python=true" >> $GITHUB_OUTPUT
else
echo "python=false" >> $GITHUB_OUTPUT
fi
# Detect integration test changes (or if any code changed)
if echo "$CHANGED" | grep -qE '^(integration-tests/|cmd/|pkg/|python/|crates/|go\.(mod|sum)|uv\.lock|pyproject\.toml)'; then
echo "integration=true" >> $GITHUB_OUTPUT
else
echo "integration=false" >> $GITHUB_OUTPUT
fi
# Detect docs changes (includes CLI source which generates docs/cli.md)
if echo "$CHANGED" | grep -qE '^(docs/|README\.md|cmd/|pkg/cli/)'; then
echo "docs=true" >> $GITHUB_OUTPUT
else
echo "docs=false" >> $GITHUB_OUTPUT
fi
fi
# Debug output
echo "Changed files:"
echo "$CHANGED" | head -50
# =============================================================================
# Version Check - Validates coglet version changes
# =============================================================================
version-check:
name: Validate coglet version
needs: changes
if: needs.changes.outputs.version_changed == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Validate version
run: |
# Get version from Cargo.toml
VERSION=$(grep '^version = ' crates/Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
echo "Coglet version: $VERSION"
# Validate semver format
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
echo "::error::Invalid version format: $VERSION"
echo "::error::Expected semver format: MAJOR.MINOR.PATCH or MAJOR.MINOR.PATCH-prerelease"
exit 1
fi
echo "✓ Valid semver format"
# Check version doesn't already exist as a tag
if git tag -l "v$VERSION" | grep -q .; then
echo "::error::Tag v$VERSION already exists!"
echo "::error::Cannot set version to an already-released version."
exit 1
fi
echo "✓ Version not yet released"
# Get the highest existing stable version tag
HIGHEST_TAG=$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' | grep -v '-' | sed 's/^v//' | sort -V | tail -1)
if [ -n "$HIGHEST_TAG" ]; then
echo "Highest released version: $HIGHEST_TAG"
# Check it's not a downgrade (using sort -V for proper semver comparison)
BASE_VERSION="${VERSION%%-*}"
SORTED_HIGHEST=$(printf '%s\n%s' "$HIGHEST_TAG" "$BASE_VERSION" | sort -V | tail -1)
if [ "$SORTED_HIGHEST" = "$HIGHEST_TAG" ] && [ "$HIGHEST_TAG" != "$BASE_VERSION" ]; then
echo "::error::Cannot downgrade version from $HIGHEST_TAG to $VERSION"
echo "::error::New version must be greater than the highest released version."
exit 1
fi
echo "✓ Version is not a downgrade"
else
echo "No existing version tags found"
fi
echo ""
echo "✓ Version $VERSION is valid for release"
# =============================================================================
# Build Stage - Produces artifacts for downstream jobs
# =============================================================================
build-sdk:
name: Build SDK
needs: changes
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.go == 'true' || needs.changes.outputs.integration == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: astral-sh/setup-uv@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: dtolnay/rust-toolchain@stable
- run: rustup default stable
- uses: taiki-e/install-action@cargo-binstall
- uses: jdx/mise-action@v3
with:
cache: false
- name: Build SDK
run: mise run ci:build:sdk
- name: Upload SDK package
uses: actions/upload-artifact@v6
with:
name: CogPackage
path: dist/cog-*
build-rust:
name: Build coglet wheel
needs: changes
if: needs.changes.outputs.rust == 'true' || needs.changes.outputs.integration == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: astral-sh/setup-uv@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: crates -> target
save-if: ${{ github.ref == 'refs/heads/main' }}
# No mise needed - maturin-action bundles maturin and zig
# Explicitly request MINIMUM_PYTHON inside the manylinux container so
# maturin produces an ABI3 wheel (cp310-abi3). Without this, maturin
# picks up the container's default Python (3.8), which doesn't support
# ABI3, producing a cp38-cp38 wheel that the upload glob won't match.
- name: Build coglet wheel (ABI3)
uses: PyO3/maturin-action@v1
with:
target: x86_64-unknown-linux-gnu
args: --release --out dist -m crates/coglet-python/Cargo.toml --interpreter python${{ env.MINIMUM_PYTHON }}
manylinux: auto
- name: Verify ABI3 wheel exists
run: |
CPVER="cp${MINIMUM_PYTHON//.}"
ls -la dist/coglet-*-${CPVER}-abi3-*.whl
- name: Upload coglet wheel
uses: actions/upload-artifact@v6
with:
name: CogletRustWheel
# ABI3 wheels use cpXYZ-abi3 naming; just match any abi3 wheel
path: dist/coglet-*-abi3-*.whl
build-cog:
name: Build cog CLI
needs: changes
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.integration == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache-dependency-path: go.sum
- uses: mlugg/setup-zig@v2
with:
version: 0.15.2
- name: Get version from Cargo.toml
id: version
run: echo "version=$(grep '^version' crates/Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')" >> "$GITHUB_OUTPUT"
- name: Build cog binary
uses: goreleaser/goreleaser-action@v7
with:
version: '~> v2'
args: build --clean --snapshot --single-target --id cog --output cog
env:
GOFLAGS: -buildvcs=false
# Use Cargo.toml as version source so snapshot builds match the wheel version
COG_VERSION: ${{ steps.version.outputs.version }}
- name: Upload cog binary
uses: actions/upload-artifact@v6
with:
name: CogBinary
path: cog
# =============================================================================
# Format Checks - Fast, parallel
# =============================================================================
fmt-go:
name: Format Go
needs: changes
if: needs.changes.outputs.go == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- run: rustup default stable
- uses: taiki-e/install-action@cargo-binstall
- uses: jdx/mise-action@v3
with:
cache: false
- name: Check Go formatting
run: mise run fmt:go
fmt-rust:
name: Format Rust
needs: changes
if: needs.changes.outputs.rust == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
# No mise needed - rustfmt comes with toolchain
- name: Check Rust formatting
run: cargo fmt --manifest-path crates/Cargo.toml --all -- --check
fmt-python:
name: Format Python
needs: changes
if: needs.changes.outputs.python == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: dtolnay/rust-toolchain@stable
- run: rustup default stable
- uses: taiki-e/install-action@cargo-binstall
- uses: jdx/mise-action@v3
with:
cache: false
- name: Check Python formatting
run: mise run fmt:python
check-llm-docs:
name: Check LLM docs
needs: changes
if: needs.changes.outputs.docs == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- run: rustup default stable
- uses: taiki-e/install-action@cargo-binstall
- uses: jdx/mise-action@v3
with:
cache: false
- name: Check llms.txt is up to date
run: mise run docs:llm:check
- name: Check CLI docs are up to date
run: mise run docs:cli:check
# =============================================================================
# Lint Checks - Parallel
# =============================================================================
lint-go:
name: Lint Go
needs: changes
if: needs.changes.outputs.go == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- uses: golangci/golangci-lint-action@v9
with:
version: v2.10.1
lint-rust:
name: Lint Rust
needs: changes
if: needs.changes.outputs.rust == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
with:
workspaces: crates -> target
save-if: ${{ github.ref == 'refs/heads/main' }}
# No mise needed - clippy comes with toolchain
- name: Lint Rust (clippy)
run: cargo clippy --manifest-path crates/Cargo.toml --workspace -- -D warnings
lint-rust-deny:
name: Lint Rust (deny)
needs: changes
if: needs.changes.outputs.rust == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@v2
with:
tool: cargo-deny@0.19.0
# No mise needed - cargo-deny installed via taiki-e
- name: Check licenses and advisories
run: cargo deny --manifest-path crates/Cargo.toml check
lint-python:
name: Lint Python
needs: [changes, build-sdk, build-rust]
if: |
needs.changes.outputs.python == 'true' &&
(needs.build-rust.result == 'success' || needs.build-rust.result == 'skipped')
runs-on: ubuntu-latest-8-cores
timeout-minutes: 15
steps:
- name: Download SDK
uses: actions/download-artifact@v8
with:
name: CogPackage
path: dist
- name: Download coglet wheel
uses: actions/download-artifact@v8
with:
name: CogletRustWheel
path: dist
if: needs.build-rust.result == 'success'
- name: Extract source distribution
run: tar xf dist/*.tar.gz --strip-components=1
- uses: astral-sh/setup-uv@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: dtolnay/rust-toolchain@stable
- run: rustup default stable
- uses: taiki-e/install-action@cargo-binstall
- uses: jdx/mise-action@v3
with:
cache: false
- name: Lint Python
run: mise run lint:python
# =============================================================================
# Test Jobs
# =============================================================================
test-go:
name: "Test Go (${{ matrix.platform }})"
needs: changes
if: needs.changes.outputs.go == 'true'
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
platform: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
# gotestsum via Go module proxy (not GitHub Releases) for reliability
- name: Install gotestsum
run: go install gotest.tools/gotestsum@v1.13.0
- name: Test Go
shell: bash
run: |
set -euo pipefail
set -m # job control, ensures script is in its own process group
cleanup() {
echo "::warning::Cancelling..."
kill -TERM -- -$$ 2>/dev/null || true
sleep 5
kill -KILL -- -$$ 2>/dev/null || true
}
trap cleanup INT TERM
gotestsum -- -short -timeout 1200s -parallel 5 ./... &
wait $!
fuzz-go:
name: Fuzz Go
needs: changes
if: needs.changes.outputs.go == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
env:
CGO_ENABLED: "1"
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Fuzz schema type resolution
run: go test ./pkg/schema/ -run='^$' -fuzz=FuzzResolveSchemaType -fuzztime=30s
- name: Fuzz JSON schema generation
run: go test ./pkg/schema/ -run='^$' -fuzz=FuzzJSONSchema -fuzztime=30s
- name: Fuzz Python parser
run: go test ./pkg/schema/python/ -run='^$' -fuzz=FuzzParsePredictor -fuzztime=30s
- name: Fuzz type annotation parsing
run: go test ./pkg/schema/python/ -run='^$' -fuzz=FuzzParseTypeAnnotation -fuzztime=30s
test-rust:
name: Test Rust
needs: changes
if: needs.changes.outputs.rust == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@v2
with:
tool: cargo-nextest@0.9.120
- uses: Swatinem/rust-cache@v2
with:
workspaces: crates -> target
save-if: ${{ github.ref == 'refs/heads/main' }}
# No mise needed - cargo-nextest installed via taiki-e
- name: Test Rust
run: cargo nextest run --manifest-path crates/Cargo.toml --workspace --exclude coglet-python --no-tests=pass
test-python:
name: "Test Python ${{ matrix.python-version }}"
needs: [changes, build-sdk, build-rust]
if: |
needs.changes.outputs.python == 'true' &&
needs.build-sdk.result == 'success' &&
(needs.build-rust.result == 'success' || needs.build-rust.result == 'skipped')
runs-on: ubuntu-latest-8-cores
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
python-version: ${{ fromJSON(needs.changes.outputs.supported_pythons) }}
steps:
- name: Download artifacts
uses: actions/download-artifact@v8
with:
path: dist
merge-multiple: true
- name: Extract source distribution
run: tar xf dist/*.tar.gz --strip-components=1
- uses: astral-sh/setup-uv@v6
with:
python-version: ${{ matrix.python-version }}
- uses: dtolnay/rust-toolchain@stable
- run: rustup default stable
- uses: taiki-e/install-action@cargo-binstall
- uses: jdx/mise-action@v3
- name: Remove src to ensure tests run against wheel
run: rm -rf python/cog
- name: Test Python
run: uvx nox -s tests -p ${{ matrix.python-version }}
test-coglet-python:
name: "Test coglet-python (${{ matrix.python-version }})"
needs: [changes, build-rust]
if: |
always() &&
(needs.changes.outputs.rust == 'true' || needs.changes.outputs.python == 'true') &&
(needs.build-rust.result == 'success' || needs.build-rust.result == 'skipped')
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
python-version: ${{ fromJSON(needs.changes.outputs.supported_pythons) }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Download coglet wheel
uses: actions/download-artifact@v8
with:
name: CogletRustWheel
path: dist
if: needs.build-rust.result == 'success'
- uses: dtolnay/rust-toolchain@stable
- run: rustup default stable # Required for cargo-binstall to find cargo
- uses: taiki-e/install-action@cargo-binstall
- uses: Swatinem/rust-cache@v2
with:
workspaces: crates -> target
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@v6
with:
python-version: ${{ matrix.python-version }}
- name: Test coglet-python bindings
run: uvx nox -s coglet -p ${{ matrix.python-version }}
# Compute integration test shards dynamically.
# Slow tests (tagged with [short] skip) are distributed round-robin first,
# then remaining tests fill in. This ensures slow tests don't pile up on one runner.
integration-shards:
name: Compute test shards
needs: changes
if: needs.changes.outputs.integration == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
shards: ${{ steps.shard.outputs.shards }}
steps:
- uses: actions/checkout@v6
- name: Compute shards
id: shard
run: |
NUM_SHARDS=${{ env.NUM_IT_RUNNER_SHARDS }}
# Find unconditionally skipped tests (bare "skip" without condition brackets)
# These are disabled tests that shouldn't affect shard distribution
SKIPPED_TESTS=$(grep -rl '^skip ' integration-tests/tests/*.txtar | \
xargs -I{} basename {} .txtar | sort || echo "")
# Identify slow tests (have [short] skip marker), excluding unconditionally skipped
SLOW_TESTS=$(grep -rl '\[short\] skip' integration-tests/tests/*.txtar | \
xargs -I{} basename {} .txtar | sort)
if [ -n "$SKIPPED_TESTS" ]; then
SLOW_TESTS=$(comm -23 <(echo "$SLOW_TESTS") <(echo "$SKIPPED_TESTS"))
fi
# All tests
ALL_TESTS=$(ls integration-tests/tests/*.txtar | \
xargs -I{} basename {} .txtar | sort)
# Fast tests = all - slow (skipped tests end up here but run instantly)
FAST_TESTS=$(comm -23 <(echo "$ALL_TESTS") <(echo "$SLOW_TESTS"))
# Distribute slow tests round-robin across shards
declare -a SHARDS
for i in $(seq 0 $((NUM_SHARDS - 1))); do
SHARDS[$i]=""
done
idx=0
while IFS= read -r test; do
[ -z "$test" ] && continue
if [ -n "${SHARDS[$idx]}" ]; then
SHARDS[$idx]="${SHARDS[$idx]}|${test}"
else
SHARDS[$idx]="$test"
fi
idx=$(( (idx + 1) % NUM_SHARDS ))
done <<< "$SLOW_TESTS"
# Distribute fast tests round-robin across shards
while IFS= read -r test; do
[ -z "$test" ] && continue
if [ -n "${SHARDS[$idx]}" ]; then
SHARDS[$idx]="${SHARDS[$idx]}|${test}"
else
SHARDS[$idx]="$test"
fi
idx=$(( (idx + 1) % NUM_SHARDS ))
done <<< "$FAST_TESTS"
# Build JSON array of shard objects
JSON="["
for i in $(seq 0 $((NUM_SHARDS - 1))); do
PATTERN="${SHARDS[$i]}"
COUNT=$(echo "$PATTERN" | tr '|' '\n' | wc -l | tr -d ' ')
[ $i -gt 0 ] && JSON="${JSON},"
JSON="${JSON}{\"index\":$i,\"pattern\":\"${PATTERN}\",\"count\":$COUNT}"
done
JSON="${JSON}]"
echo "shards=$JSON" >> "$GITHUB_OUTPUT"
# Debug output
echo "Shard distribution:"
for i in $(seq 0 $((NUM_SHARDS - 1))); do
COUNT=$(echo "${SHARDS[$i]}" | tr '|' '\n' | wc -l | tr -d ' ')
SLOW_COUNT=$(echo "${SHARDS[$i]}" | tr '|' '\n' | while read t; do
echo "$SLOW_TESTS" | grep -q "^${t}$" && echo "$t"
done | wc -l | tr -d ' ')
echo " Shard $i: $COUNT tests ($SLOW_COUNT slow)"
done
test-integration:
name: "Test integration (shard ${{ matrix.shard.index }})"
needs: [changes, build-cog, build-sdk, build-rust, integration-shards]
if: |
!cancelled() &&
needs.changes.outputs.integration == 'true' &&
needs.integration-shards.result == 'success' &&
(needs.build-cog.result == 'success' || needs.build-cog.result == 'skipped') &&
(needs.build-sdk.result == 'success' || needs.build-sdk.result == 'skipped') &&
(needs.build-rust.result == 'success' || needs.build-rust.result == 'skipped')
runs-on: ubuntu-latest-16-cores
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
shard: ${{ fromJSON(needs.integration-shards.outputs.shards) }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Login to Docker Hub
uses: docker/login-action@v3
if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name != 'pull_request'
with:
registry: index.docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Download artifacts
uses: actions/download-artifact@v8
with:
path: dist
merge-multiple: true
- name: Install cog binary
run: |
cp dist/cog ./cog
chmod +x ./cog
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache-dependency-path: go.sum
# gotestsum via Go module proxy (not GitHub Releases) for reliability
- name: Install gotestsum
run: go install gotest.tools/gotestsum@v1.13.0
- name: Set wheel environment
run: |
# Use locally-built wheels, not PyPI (version may not be published yet)
# Must use absolute paths — cog subprocess runs from txtar workdir, not checkout root
echo "COG_SDK_WHEEL=${{ github.workspace }}/dist" >> $GITHUB_ENV
echo "COGLET_WHEEL=${{ github.workspace }}/dist" >> $GITHUB_ENV
- name: Run integration tests (shard ${{ matrix.shard.index }}, ${{ matrix.shard.count }} tests)
env:
COG_BINARY: ./cog
TEST_PARALLEL: 4
BUILDKIT_PROGRESS: 'quiet'
shell: bash
run: |
set -euo pipefail
set -m # job control, ensures script is in its own process group
cleanup() {
echo "::warning::Cancelling..."
kill -TERM -- -$$ 2>/dev/null || true
sleep 5
kill -KILL -- -$$ 2>/dev/null || true
}
trap cleanup INT TERM
# Build -run regex from shard pattern
# Pattern is "test1|test2|test3" - wrap each in TestIntegration/<name>/
RUN_PATTERN="${{ matrix.shard.pattern }}"
echo "Running tests matching: $RUN_PATTERN"
gotestsum --format github-actions -- \
-tags integration \
-parallel $TEST_PARALLEL \
-timeout 30m \
-run "TestIntegration/($RUN_PATTERN)/" \
./integration-tests/... &
wait $!
# =============================================================================
# Gate Job - Single required check for branch protection
# =============================================================================
ci-complete:
name: CI Complete
needs:
- changes
- version-check
- build-cog
- build-sdk
- build-rust
- fmt-go
- fmt-rust
- fmt-python
- check-llm-docs
- lint-go
- lint-rust
- lint-rust-deny
- lint-python
- test-go
- test-rust
- test-python
- test-coglet-python
- integration-shards
- test-integration
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Check job results
run: |
echo "Job results:"
echo " changes: ${{ needs.changes.result }}"
echo " build-sdk: ${{ needs.build-sdk.result }}"
echo " build-rust: ${{ needs.build-rust.result }}"
echo " fmt-go: ${{ needs.fmt-go.result }}"
echo " fmt-rust: ${{ needs.fmt-rust.result }}"
echo " fmt-python: ${{ needs.fmt-python.result }}"
echo " check-llm-docs: ${{ needs.check-llm-docs.result }}"
echo " lint-go: ${{ needs.lint-go.result }}"
echo " lint-rust: ${{ needs.lint-rust.result }}"
echo " lint-rust-deny: ${{ needs.lint-rust-deny.result }}"
echo " lint-python: ${{ needs.lint-python.result }}"
echo " test-go: ${{ needs.test-go.result }}"
echo " test-rust: ${{ needs.test-rust.result }}"
echo " test-python: ${{ needs.test-python.result }}"
echo " test-coglet-python: ${{ needs.test-coglet-python.result }}"
echo " integration-shards: ${{ needs.integration-shards.result }}"
echo " test-integration: ${{ needs.test-integration.result }}"
# Fail if any job failed (skipped is OK)
FAILED=false
for result in \
"${{ needs.changes.result }}" \
"${{ needs.build-sdk.result }}" \
"${{ needs.build-rust.result }}" \
"${{ needs.fmt-go.result }}" \
"${{ needs.fmt-rust.result }}" \
"${{ needs.fmt-python.result }}" \
"${{ needs.check-llm-docs.result }}" \
"${{ needs.lint-go.result }}" \
"${{ needs.lint-rust.result }}" \
"${{ needs.lint-rust-deny.result }}" \
"${{ needs.lint-python.result }}" \
"${{ needs.test-go.result }}" \
"${{ needs.test-rust.result }}" \
"${{ needs.test-python.result }}" \
"${{ needs.test-coglet-python.result }}" \
"${{ needs.integration-shards.result }}" \
"${{ needs.test-integration.result }}"
do
if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then
FAILED=true
fi
done
if [ "$FAILED" = "true" ]; then
echo "::error::Some jobs failed or were cancelled"
exit 1
fi
echo "All CI checks passed!"
# =============================================================================
# Release Validation - Dry-run checks (PRs and main)
# =============================================================================
release-dry-run:
name: Release Dry Run
needs: ci-complete
if: "!startsWith(github.ref, 'refs/tags/')"
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: crates -> target
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Check coglet crates.io publish
run: cargo publish --dry-run -p coglet --manifest-path crates/Cargo.toml
- uses: mlugg/setup-zig@v2
with:
version: 0.15.2
- uses: goreleaser/goreleaser-action@v7
with:
version: '~> v2'
args: check