name: CI
on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
- develop
release:
types:
- published
workflow_dispatch:
inputs:
test-release:
description: "Simulate a release"
required: false
default: "true"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
RUSTC_VERSION: 1.90.0
jobs:
cancel-previous:
name: Cancel Previous Runs
runs-on: ubuntu-latest
steps:
- name: Cancel Previous Runs
uses: styfle/cancel-workflow-action@0.12.1
with:
access_token: ${{ github.token }}
changes:
name: Detect Changes
runs-on: ubuntu-latest
needs: cancel-previous
outputs:
docs: ${{ steps.filter.outputs.docs }}
rust: ${{ steps.filter.outputs.rust }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
- name: Detect changed projects
id: filter
uses: dorny/paths-filter@v3
with:
filters: |
docs:
- 'docs/**'
- '.github/workflows/ci.yaml'
rust:
- 'Cargo.toml'
- 'Cargo.lock'
- 'src/**'
- 'tests/**'
- 'examples/**'
- 'ci/**'
- 'clippy.toml'
- 'rustfmt.toml'
- 'rust-toolchain.toml'
- 'Dockerfile.builder'
- 'config.yaml'
- '.github/workflows/ci.yaml'
extract-version:
name: Extract Version
runs-on: ubuntu-latest
needs: cancel-previous
outputs:
current_version: ${{ steps.extract.outputs.current_version }}
tag: ${{ steps.extract.outputs.tag }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set Up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Extract current version
id: extract
shell: bash
run: |
set -euo pipefail
current_version=$(python ci/extract_version.py)
echo "current_version=$current_version" >> "$GITHUB_OUTPUT"
echo "tag=v$current_version" >> "$GITHUB_OUTPUT"
detect-version-bump:
name: Detect Version Bump
runs-on: ubuntu-latest
needs: extract-version
outputs:
bumped: ${{ steps.detect.outputs.bumped }}
current_version: ${{ steps.detect.outputs.current_version }}
previous_version: ${{ steps.detect.outputs.previous_version }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set Up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Detect version change on main push
id: detect
shell: bash
run: |
set -euo pipefail
current_version="${{ needs.extract-version.outputs.current_version }}"
previous_version=""
bumped="false"
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/main" ]]; then
before_sha="${{ github.event.before }}"
if [[ -n "$before_sha" && "$before_sha" != "0000000000000000000000000000000000000000" ]]; then
if git cat-file -e "$before_sha:Cargo.toml" 2>/dev/null; then
previous_version=$(git show "$before_sha:Cargo.toml" | python ci/extract_version.py --stdin)
else
echo "Previous commit does not contain Cargo.toml; treating as no bump."
fi
if [[ -z "$previous_version" ]]; then
echo "Could not parse previous Cargo.toml version; treating as no bump."
elif [[ "$current_version" != "$previous_version" ]]; then
bumped="true"
fi
else
echo "No valid before SHA available; treating as no bump."
fi
fi
echo "current_version=$current_version" >> "$GITHUB_OUTPUT"
echo "previous_version=$previous_version" >> "$GITHUB_OUTPUT"
echo "bumped=$bumped" >> "$GITHUB_OUTPUT"
echo "Current version: $current_version"
echo "Previous version: ${previous_version:-<none>}"
echo "Version bumped: $bumped"
docs:
name: Mintlify Docs
runs-on: ubuntu-latest
needs: [cancel-previous, changes]
if: ${{ needs.changes.outputs.docs == 'true' }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set Up Node.js
uses: actions/setup-node@v4
with:
node-version: "20.17.0"
- name: Install Mintlify CLI
run: npm install --global mintlify@4.0.373
- name: Validate Docs
working-directory: docs
run: mintlify broken-links
rustdoc:
name: Rust Doc
runs-on: ubuntu-latest
needs: [cancel-previous, changes]
if: ${{ needs.changes.outputs.rust == 'true' }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo Dependencies
uses: Swatinem/rust-cache@v2
- name: Check Documentation
run: cargo rustdoc --lib -- -D missing_docs
cargo-sort:
name: Cargo Sort
runs-on: ubuntu-latest
needs: [changes, rustdoc]
if: ${{ needs.changes.outputs.rust == 'true' }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo Dependencies
uses: Swatinem/rust-cache@v2
- name: Install cargo-sort
run: cargo install cargo-sort
- name: Check Cargo.toml Formatting
run: cargo sort --check --workspace
rustfmt:
name: Rustfmt
runs-on: ubuntu-latest
needs: [changes, rustdoc]
if: ${{ needs.changes.outputs.rust == 'true' }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Rust Nightly
uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- name: Cache Cargo Dependencies
uses: Swatinem/rust-cache@v2
- name: Run Rustfmt
run: cargo +nightly fmt -- --check
clippy:
name: Clippy
runs-on: ubuntu-latest
needs: [changes, cargo-sort, rustfmt]
if: ${{ needs.changes.outputs.rust == 'true' }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo Dependencies
uses: Swatinem/rust-cache@v2
- name: Install clippy
run: rustup component add clippy
- name: Run Clippy
run: cargo clippy --all-targets --all-features -- -D warnings
cargo-build:
name: Cargo Build
runs-on: ubuntu-latest
needs: [changes, clippy]
if: ${{ needs.changes.outputs.rust == 'true' }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo Dependencies
uses: Swatinem/rust-cache@v2
- name: Cargo build
run: cargo build --locked
cargo-test:
name: Cargo Test
runs-on: ubuntu-latest
needs: [changes, clippy]
if: ${{ needs.changes.outputs.rust == 'true' }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo Dependencies
uses: Swatinem/rust-cache@v2
- name: Run Tests
run: cargo test --verbose
docker-uat-tests:
name: Docker UAT Tests
runs-on: ubuntu-latest
needs: [changes, clippy]
if: false strategy:
matrix:
test-mode: [user, kernel]
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo Dependencies
uses: Swatinem/rust-cache@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image (${{ matrix.test-mode }})
run: |
# Build the Docker image using CI-specific Dockerfile
docker build -f tests/Dockerfile.${{ matrix.test-mode }}.ci -t systemg-${{ matrix.test-mode }}:ci .
- name: Run Docker UAT tests (${{ matrix.test-mode }})
id: run-tests
run: |
set -e
# Create a directory for test output
mkdir -p test-output
# Run the Docker container with appropriate settings
if [ "${{ matrix.test-mode }}" = "kernel" ]; then
# Kernel mode needs privileged access
docker run \
--rm \
--privileged \
--cap-add SYS_ADMIN \
--cap-add NET_ADMIN \
--security-opt apparmor:unconfined \
-v /sys/fs/cgroup:/sys/fs/cgroup:ro \
-v $(pwd)/test-output:/test-output \
systemg-${{ matrix.test-mode }}:ci | tee test-output/${{ matrix.test-mode }}.log
else
# User mode can run without special privileges
docker run \
--rm \
-v $(pwd)/test-output:/test-output \
systemg-${{ matrix.test-mode }}:ci | tee test-output/${{ matrix.test-mode }}.log
fi
# Extract test results for job summary
if grep -q "All.*tests passed successfully" test-output/${{ matrix.test-mode }}.log; then
echo "✅ ${{ matrix.test-mode }} mode: All tests passed" >> $GITHUB_STEP_SUMMARY
else
echo "❌ ${{ matrix.test-mode }} mode: Some tests failed" >> $GITHUB_STEP_SUMMARY
exit 1
fi
- name: Upload test logs
if: always()
uses: actions/upload-artifact@v4
with:
name: docker-${{ matrix.test-mode }}-logs
path: test-output/${{ matrix.test-mode }}.log
create-github-release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs:
- cargo-test
- extract-version
- detect-version-bump
permissions:
contents: write
if: >-
github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.detect-version-bump.outputs.bumped == 'true'
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Create or Update release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.extract-version.outputs.tag }}
name: ${{ needs.extract-version.outputs.tag }}
target_commitish: ${{ github.sha }}
generate_release_notes: true
append_body: false
make_latest: true
fail_on_unmatched_files: false
env:
GITHUB_TOKEN: ${{ github.token }}
upload-release-binaries:
name: Upload Release Binaries
runs-on: ${{ matrix.job.os }}
permissions:
contents: write
needs:
- cargo-test
- detect-version-bump
- create-github-release
- extract-version
if: |
!cancelled() &&
needs.cargo-test.result == 'success' &&
(
(needs.create-github-release.result == 'success') ||
(needs.create-github-release.result == 'skipped' && github.event_name == 'release' && github.event.action == 'published') ||
(github.event_name == 'workflow_dispatch')
)
strategy:
matrix:
job:
- os: ubuntu-latest
platform: linux
target: x86_64-unknown-linux-gnu
cross_image: x86_64-linux-gnu
cross_dockerfile: ci/Dockerfile.x86_64-unknown-linux-gnu-clang
features: linux
- os: ubuntu-latest
platform: linux-arm
target: aarch64-unknown-linux-gnu
cross_image: aarch64-linux-gnu
cross_dockerfile: ci/Dockerfile.aarch64-unknown-linux-gnu-clang
features: linux
- os: ubuntu-latest
platform: debian
target: x86_64-unknown-linux-gnu
cross_image: debian-x86_64-linux-gnu
cross_dockerfile: ci/Dockerfile.debian-x86_64-unknown-linux-gnu-clang
features: linux
artifact_suffix: -debian
- os: macos-latest
platform: darwin
target: x86_64-apple-darwin
- os: macos-latest
platform: darwin-arm
target: aarch64-apple-darwin
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
if: matrix.job.cross_image
uses: docker/setup-buildx-action@v3
- name: Setup custom cross env ${{ matrix.job.cross_image }}
if: matrix.job.cross_image
uses: docker/build-push-action@v5
with:
context: ci
file: ${{ matrix.job.cross_dockerfile }}
tags: ${{ matrix.job.cross_image }}:latest
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Install packages (macOS)
if: matrix.job.os == 'macos-latest'
run: |
ci/macos-install-packages.sh
- name: Install toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ env.RUSTC_VERSION }}
targets: ${{ matrix.job.target }}
- name: Install cross
uses: baptiste0928/cargo-install@v3
with:
crate: cross
cache-key: ${{ matrix.job.target }}
version: "0.2.5"
- name: Build sysg
run: |
cross \
build \
--profile=release \
--target ${{ matrix.job.target }}
- name: Strip release binary linux x86_64
if: matrix.job.target == 'x86_64-unknown-linux-gnu'
run: >
strip "target/${{ matrix.job.target }}/release/sysg"
- name: Strip release binary aarch64-linux-gnu
if: matrix.job.target == 'aarch64-unknown-linux-gnu'
run: |
docker run --rm -v \
"$PWD/target:/target:Z" \
aarch64-linux-gnu:latest \
aarch64-linux-gnu-strip \
/target/aarch64-unknown-linux-gnu/release/sysg
- name: Strip release binary mac
if: matrix.job.os == 'macos-latest'
run: |
strip -x "target/${{ matrix.job.target }}/release/sysg"
- name: Prepare Binary Artifact
env:
PLATFORM_NAME: ${{ matrix.job.platform }}
TARGET: ${{ matrix.job.target }}
ARTIFACT_SUFFIX: ${{ matrix.job.artifact_suffix || '' }}
run: >
# Get version from either the tag (for auto-release) or GITHUB_REF (for manual releases)
if [ "${{ needs.extract-version.outputs.tag }}" != "" ]; then
SYSG_VERSION="${{ needs.extract-version.outputs.tag }}"
else
SYSG_VERSION="${GITHUB_REF#refs/tags/}"
fi
# optionally trim v from tag prefix
SYSG_VERSION="${SYSG_VERSION#v}"
echo "version is: $SYSG_VERSION"
# setup artifact filename
SYSG_ARTIFACT="sysg-$SYSG_VERSION-$TARGET$ARTIFACT_SUFFIX"
SYSG_ZIP_FILE_NAME="$SYSG_ARTIFACT.tar.gz"
echo "SYSG_ZIP_FILE_NAME=$SYSG_ZIP_FILE_NAME" >> $GITHUB_ENV
# create zip file
mkdir -pv "$SYSG_ARTIFACT"
cp "target/${{ matrix.job.target }}/release/sysg" "$SYSG_ARTIFACT"
tar -czvf "$SYSG_ZIP_FILE_NAME" "$SYSG_ARTIFACT"
- name: Upload Sysg Artifact
uses: softprops/action-gh-release@v1
if: github.event_name == 'release' || needs.create-github-release.result == 'success'
env:
GITHUB_TOKEN: ${{ github.token }}
with:
tag_name: ${{ needs.extract-version.outputs.tag }}
files: ./${{ env.SYSG_ZIP_FILE_NAME }}
upload-s3:
name: Upload Binaries to S3
needs:
- upload-release-binaries
- extract-version
if: |
!cancelled() &&
needs.upload-release-binaries.result == 'success'
environment: ci
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download and upload release assets to AWS S3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ needs.extract-version.outputs.tag }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-2 S3_BUCKET: sh.sysg.dev
run: |
set -euo pipefail
# Get release ID from tag (works for both auto-release and manual releases)
if [ -n "${{ github.event.release.id }}" ]; then
RELEASE_ID="${{ github.event.release.id }}"
else
# Fetch release ID by tag for auto-releases
RELEASE_ID=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${{ github.repository }}/releases/tags/$RELEASE_TAG" \
| jq -r '.id')
fi
echo "Fetching .tar.gz assets for release ID $RELEASE_ID ..."
while read -r url name; do
echo "Downloading $name from $url ..."
curl -sSL -H "Authorization: token $GITHUB_TOKEN" -o "$name" "$url"
echo "Uploading $name to s3://${S3_BUCKET}/${name} ..."
aws s3 cp "$name" "s3://${S3_BUCKET}/${name}" --content-type "application/gzip"
echo "Uploaded $name successfully"
done < <(
curl -s -H "Authorization: token $GITHUB_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${{ github.repository }}/releases/$RELEASE_ID/assets" \
| jq -r '.[] | select(.name | endswith(".tar.gz")) | "\(.browser_download_url) \(.name)"'
)
cargo-publish:
name: Publish to crates.io
runs-on: ubuntu-latest
needs:
- cargo-test
- detect-version-bump
- create-github-release
- upload-s3
if: |
!cancelled() &&
needs.cargo-test.result == 'success' &&
needs.upload-s3.result == 'success' &&
(
(
github.event_name == 'push' &&
github.ref == 'refs/heads/main' &&
needs.create-github-release.result == 'success'
) ||
(github.event_name == 'workflow_dispatch')
)
environment: ci
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Publish Crate
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATESIO_TOKEN }}
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ "${{ github.event.inputs.test-release }}" == "true" ]; then
cargo publish --dry-run --locked --no-verify
else
cargo publish --locked --no-verify
fi