Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
228 changes: 228 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
name: Build - Release

on:
# main builds (no publish) so the release cache is seeded on main for PRs to restore.
push:
branches: [main]
tags: ["v*"]
pull_request:
branches: [main]
paths:
- "src/**"
- "tests/**"
- "Cargo.toml"
- "Cargo.lock"
- ".github/workflows/release.yml"
workflow_dispatch:
inputs:
bump:
description: "Version bump type (leave 'auto' to detect from commit messages)"
required: true
type: choice
default: auto
options:
- auto
- patch
- minor
- major

permissions:
contents: write

# On pull requests the build jobs run (to validate the release build) but compute/publish
# don't; cancel superseded PR runs. Tag/dispatch runs key on run_id, so they never cancel.
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: "1"

jobs:
# Dispatch-only: work out the next version. Nothing is written or pushed here — the
# version is stamped into each build's working tree, and only committed + tagged by
# `publish` after the builds succeed, so a failed build never bumps the version or
# leaves a dangling tag.
compute:
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
version: ${{ steps.version.outputs.new }}
tag: ${{ steps.version.outputs.tag }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0 # history + tags for `git describe`

- name: Detect bump type from commits
id: detect
run: |
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
RANGE="HEAD"
[ -n "$LAST_TAG" ] && RANGE="${LAST_TAG}..HEAD"
COMMITS=$(git log --pretty=format:"%s" "$RANGE")
BUMP="patch"
if echo "$COMMITS" | grep -qiE "^feat(\(.*\))?!:|BREAKING CHANGE"; then
BUMP="major"
elif echo "$COMMITS" | grep -qiE "^feat(\(.*\))?:"; then
BUMP="minor"
elif echo "$COMMITS" | grep -qiE "^(fix|perf)(\(.*\))?:"; then
BUMP="patch"
fi
echo "detected=$BUMP" >> "$GITHUB_OUTPUT"

- name: Resolve bump type
id: bump
run: |
BUMP="${{ inputs.bump }}"
[ "$BUMP" = "auto" ] && BUMP="${{ steps.detect.outputs.detected }}"
echo "type=$BUMP" >> "$GITHUB_OUTPUT"

- name: Compute new version
id: version
run: |
CURRENT=$(grep -m1 '^version' Cargo.toml | sed 's/.*"\(.*\)"/\1/')
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
case "${{ steps.bump.outputs.type }}" in
major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
patch) PATCH=$((PATCH + 1)) ;;
esac
NEW="${MAJOR}.${MINOR}.${PATCH}"
echo "new=$NEW" >> "$GITHUB_OUTPUT"
echo "tag=v${NEW}" >> "$GITHUB_OUTPUT"

build-linux:
name: Build Linux (${{ matrix.arch }})
needs: [compute]
if: |
!cancelled() &&
(startsWith(github.ref, 'refs/tags/') || needs.compute.result == 'success'
|| github.event_name == 'pull_request' || github.ref == 'refs/heads/main')
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
arch: x86_64
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
targets: ${{ matrix.target }}

- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: release-${{ matrix.target }}

# Stamp the computed version into the working tree (no commit) so the binary embeds
# it via CARGO_PKG_VERSION. Skipped on tag/PR builds, which use Cargo.toml as-is.
- name: Set release version
if: needs.compute.outputs.version != ''
run: perl -i -pe 'if (!$d && s/^version = ".*"/version = "${{ needs.compute.outputs.version }}"/) { $d = 1 }' Cargo.toml

- name: Build
run: cargo build --release --target ${{ matrix.target }}

- name: Package
run: |
mkdir -p dist
cp "target/${{ matrix.target }}/release/agentcap" "dist/agentcap-${{ matrix.arch }}-linux"
chmod +x dist/*

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: linux-${{ matrix.arch }}
path: dist/

build-macos:
name: Build macOS (arm64)
needs: [compute]
if: |
!cancelled() &&
(startsWith(github.ref, 'refs/tags/') || needs.compute.result == 'success'
|| github.event_name == 'pull_request' || github.ref == 'refs/heads/main')
runs-on: macos-14
timeout-minutes: 45
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
targets: aarch64-apple-darwin

- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: release-aarch64-apple-darwin

- name: Set release version
if: needs.compute.outputs.version != ''
run: perl -i -pe 'if (!$d && s/^version = ".*"/version = "${{ needs.compute.outputs.version }}"/) { $d = 1 }' Cargo.toml

- name: Build
run: cargo build --release --target aarch64-apple-darwin

- name: Package
run: |
mkdir -p dist
cp target/aarch64-apple-darwin/release/agentcap dist/agentcap-arm64-apple-darwin
chmod +x dist/*

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: macos-arm64
path: dist/

# Runs only after both builds succeed. On dispatch it commits the version bump, tags,
# and pushes — so the version/tag only ever advance for a release that actually built.
# On a tag push there's nothing to commit; it just publishes at that tag.
publish:
name: Publish Release
needs: [compute, build-linux, build-macos]
if: |
!cancelled() &&
needs.build-linux.result == 'success' &&
needs.build-macos.result == 'success' &&
(startsWith(github.ref, 'refs/tags/') || needs.compute.result == 'success')
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

# Dispatch only: bump now that the builds passed, matching the version stamped into
# the artifacts, then tag and push.
- name: Commit and tag release
if: needs.compute.outputs.version != ''
run: |
perl -i -pe 'if (!$d && s/^version = ".*"/version = "${{ needs.compute.outputs.version }}"/) { $d = 1 }' Cargo.toml
perl -i -pe 'if ($h) { s/^version = .*/version = "${{ needs.compute.outputs.version }}"/; $h = 0 } $h = 1 if /^name = "agentcap"$/' Cargo.lock
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Cargo.toml Cargo.lock
git commit -m "chore: release ${{ needs.compute.outputs.tag }}"
git tag "${{ needs.compute.outputs.tag }}"
git push origin HEAD:main --tags

- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: artifacts
pattern: "{linux-*,macos-*}"
merge-multiple: true

- name: Create release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${{ needs.compute.outputs.tag || github.ref_name }}"
gh release create "$TAG" artifacts/* \
--repo "${{ github.repository }}" \
--title "$TAG" \
--generate-notes
72 changes: 72 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: Test - Unit & Integration

# Hermetic tests only: unit tests + the loopback proxy integration test. No
# network, podman, or model server. The full agent×model end-to-end ("live")
# tests are a separate, resource-heavy category — see `linux-live-tests.yml`.

on:
pull_request:
branches: [main]
paths:
- "src/**"
- "tests/**"
- "Cargo.toml"
- "Cargo.lock"
- "rustfmt.toml"
- ".github/workflows/test.yml"
push:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

permissions:
contents: read

env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: "1"

jobs:
lint-unit:
name: Lint & Unit Tests
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
components: rustfmt, clippy

- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
shared-key: test

- name: Format check
run: cargo fmt --check

- name: Clippy
run: cargo clippy --all-targets -- -D warnings

- name: Unit tests
run: cargo test --lib

integration:
name: Integration Tests
needs: lint-unit
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

- uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0

- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
shared-key: test

# Spins a mock upstream over loopback and drives CaptureProxy through it.
- name: Integration tests
run: cargo test --test proxy
5 changes: 5 additions & 0 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Pin the toolchain so local `cargo fmt/clippy/test` matches CI (which pins the
# same version in .github/workflows). Bump here and in the workflows together.
[toolchain]
channel = "1.95.0"
components = ["rustfmt", "clippy"]
2 changes: 1 addition & 1 deletion src/inspect/sources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ pub fn enumerate_workspace_requests(run_dir: &Path) -> Vec<ReqRow> {
let row = build_req_row(&run_id, rid, req, status, &mut chains);
rows.push(row);
}
rows.sort_by(|a, b| (a.run_id.clone(), a.captured_at).cmp(&(b.run_id.clone(), b.captured_at)));
rows.sort_by_key(|a| (a.run_id.clone(), a.captured_at));
rows
}

Expand Down
Loading