# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. name: CI on: push: branches: [main] pull_request: branches: [main] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} env: CARGO_TERM_COLOR: always SCCACHE_GHA_ENABLED: "true" SCCACHE_IGNORE_SERVER_IO_ERROR: "true" jobs: changes: name: Detect Changes runs-on: ubuntu-latest outputs: rust: ${{ steps.filter.outputs.rust }} nix: ${{ steps.filter.outputs.nix }} skills: ${{ steps.filter.outputs.skills }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 id: filter with: filters: | rust: - '**/*.rs' - 'Cargo.toml' - 'crates/*/Cargo.toml' - 'Cargo.lock' - 'build.rs' - '.cargo/**' nix: - 'flake.nix' - 'flake.lock' skills: - 'skills/**' test: name: Test needs: changes if: needs.changes.outputs.rust == 'true' || github.event_name == 'push' runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest] steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Install Rust uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable - name: Setup sccache id: sccache uses: mozilla-actions/sccache-action@2df7dbab909c49ab7d3382d05da469f3f975c2d6 # v0.0.7 continue-on-error: true - name: Enable sccache if: steps.sccache.outcome == 'success' shell: bash run: | if sccache --start-server 2>/dev/null; then echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV" else echo "::warning::sccache server failed to start, building without cache" fi - name: Cache cargo uses: Swatinem/rust-cache@ad397744b0d591a723ab90405b7247fac0e6b8db # v2 with: key: test-${{ matrix.os }} - name: Run tests run: cargo test --workspace --verbose nix: name: Nix needs: changes if: needs.changes.outputs.rust == 'true' || needs.changes.outputs.nix == 'true' || github.event_name == 'push' runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Install Nix uses: DeterminateSystems/nix-installer-action@d96bc962e61b3049ce8128d03d57a1144fa96539 # main - name: Magic Nix Cache uses: DeterminateSystems/magic-nix-cache-action@cec65ff6f104850203b152861d3f9e5f1747885d # main - name: Check flake run: nix flake check - name: Build flake run: nix build . lint: name: Lint needs: changes if: needs.changes.outputs.rust == 'true' || github.event_name == 'push' runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Install Rust uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable with: components: rustfmt, clippy - name: Setup sccache id: sccache uses: mozilla-actions/sccache-action@2df7dbab909c49ab7d3382d05da469f3f975c2d6 # v0.0.7 continue-on-error: true - name: Enable sccache if: steps.sccache.outcome == 'success' shell: bash run: | if sccache --start-server 2>/dev/null; then echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV" else echo "::warning::sccache server failed to start, building without cache" fi - name: Cache cargo uses: Swatinem/rust-cache@ad397744b0d591a723ab90405b7247fac0e6b8db # v2 with: key: lint - name: Check formatting run: | if ! cargo fmt --all -- --check; then echo "::error::Cargo fmt failed. Please run 'cargo fmt --all' locally and commit the changes." exit 1 fi - name: Clippy run: cargo clippy --workspace -- -D warnings deny: name: Cargo Deny needs: changes if: needs.changes.outputs.rust == 'true' || github.event_name == 'push' runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Cargo deny uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2.0.15 with: command: check skills: name: Verify Skills needs: changes if: needs.changes.outputs.rust == 'true' || github.event_name == 'push' runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Install Rust uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable - name: Setup sccache id: sccache uses: mozilla-actions/sccache-action@2df7dbab909c49ab7d3382d05da469f3f975c2d6 # v0.0.7 continue-on-error: true - name: Enable sccache if: steps.sccache.outcome == 'success' shell: bash run: | if sccache --start-server 2>/dev/null; then echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV" else echo "::warning::sccache server failed to start, building without cache" fi - name: Cache cargo uses: Swatinem/rust-cache@ad397744b0d591a723ab90405b7247fac0e6b8db # v2 with: key: skills - name: Regenerate skills run: cargo run -- generate-skills --output-dir skills - name: Check for drift run: | if ! git diff --exit-code skills/; then echo "::warning::Skills are out of date — the hourly auto-sync PR will fix this automatically." fi lint-skills: name: Lint Skills needs: changes if: needs.changes.outputs.skills == 'true' || github.event_name == 'push' runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5 - name: Validate skills run: | failed=0 for skill_dir in skills/*/; do if ! uvx --from skills-ref@0.1.1 agentskills validate "$skill_dir"; then failed=1 fi done if [ "$failed" -ne 0 ]; then echo "::error::One or more skills failed validation." exit 1 fi build-linux: name: Build (Linux x86_64) needs: changes if: needs.changes.outputs.rust == 'true' || github.event_name == 'push' runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Install Rust uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable with: targets: x86_64-unknown-linux-gnu - name: Setup sccache id: sccache uses: mozilla-actions/sccache-action@2df7dbab909c49ab7d3382d05da469f3f975c2d6 # v0.0.7 continue-on-error: true - name: Enable sccache if: steps.sccache.outcome == 'success' shell: bash run: | if sccache --start-server 2>/dev/null; then echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV" else echo "::warning::sccache server failed to start, building without cache" fi - name: Cache cargo uses: Swatinem/rust-cache@ad397744b0d591a723ab90405b7247fac0e6b8db # v2 with: key: build-x86_64-unknown-linux-gnu cache-targets: "false" - name: Build run: cargo build --release --target x86_64-unknown-linux-gnu - name: Upload binary uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: gws-linux-x86_64 path: target/x86_64-unknown-linux-gnu/release/gws retention-days: 1 build: name: Build needs: [smoketest, changes] if: | always() && !cancelled() && !failure() && (needs.changes.outputs.rust == 'true' || github.event_name == 'push') runs-on: ${{ matrix.os }} strategy: matrix: include: - os: ubuntu-latest target: aarch64-unknown-linux-gnu - os: macos-latest target: x86_64-apple-darwin - os: macos-latest target: aarch64-apple-darwin - os: windows-latest target: x86_64-pc-windows-msvc steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Install Rust uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable with: targets: ${{ matrix.target }} - name: Setup sccache id: sccache uses: mozilla-actions/sccache-action@2df7dbab909c49ab7d3382d05da469f3f975c2d6 # v0.0.7 continue-on-error: true - name: Enable sccache if: steps.sccache.outcome == 'success' shell: bash run: | if sccache --start-server 2>/dev/null; then echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV" else echo "::warning::sccache server failed to start, building without cache" fi - name: Cache cargo uses: Swatinem/rust-cache@ad397744b0d591a723ab90405b7247fac0e6b8db # v2 with: key: build-${{ matrix.target }} cache-targets: "false" - name: Install cross-compilation tools if: matrix.target == 'aarch64-unknown-linux-gnu' run: | sudo apt-get update sudo apt-get install -y gcc-aarch64-linux-gnu - name: Disable Windows Defender scanning for cargo if: runner.os == 'Windows' run: | Add-MpPreference -ExclusionPath "$env:USERPROFILE\.cargo" Add-MpPreference -ExclusionPath "$env:USERPROFILE\.rustup" Add-MpPreference -ExclusionPath "${{ github.workspace }}\target" - name: Build run: cargo build --release --target ${{ matrix.target }} env: CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc smoketest: name: API Smoketest needs: build-linux if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name != 'pull_request' runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Download binary uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: gws-linux-x86_64 path: ./bin - name: Make binary executable run: chmod +x ./bin/gws - name: Decode credentials env: GOOGLE_CREDENTIALS_JSON: ${{ secrets.GOOGLE_CREDENTIALS_JSON }} run: | if [ -z "$GOOGLE_CREDENTIALS_JSON" ]; then echo "::error::GOOGLE_CREDENTIALS_JSON secret is not set" exit 1 fi echo "$GOOGLE_CREDENTIALS_JSON" | base64 -d > /tmp/credentials.json - name: Smoketest — help run: ./bin/gws --help - name: Smoketest — schema introspection run: ./bin/gws schema drive.files.list | jq -e '.httpMethod' - name: Smoketest — Drive files list env: GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE: /tmp/credentials.json run: | ./bin/gws drive files list \ --params '{"pageSize": 1, "fields": "files(id,mimeType)"}' \ | jq -e '.files' - name: Smoketest — Gmail messages env: GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE: /tmp/credentials.json run: | ./bin/gws gmail users messages list \ --params '{"userId": "me", "maxResults": 1, "fields": "messages(id)"}' \ | jq -e '.messages' - name: Smoketest — Calendar events env: GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE: /tmp/credentials.json run: | ./bin/gws calendar events list \ --params '{"calendarId": "primary", "maxResults": 1, "fields": "kind,items(id,status)"}' \ | jq -e '.kind' - name: Smoketest — Slides presentation env: GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE: /tmp/credentials.json run: | ./bin/gws slides presentations get \ --params '{"presentationId": "1knOKD_87JWE4qsEbO4r5O91IxTER5ybBBhOJgZ1yLFI", "fields": "presentationId,slides(objectId)"}' \ | jq -e '.presentationId' - name: Smoketest — pagination env: GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE: /tmp/credentials.json run: | LINES=$(./bin/gws drive files list \ --params '{"pageSize": 1, "fields": "nextPageToken,files(id)"}' \ --page-all --page-limit 2 \ | wc -l) if [ "$LINES" -lt 2 ]; then echo "::error::Expected at least 2 NDJSON lines from pagination, got $LINES" exit 1 fi - name: Smoketest — error handling run: | if ./bin/gws fakeservice list 2>&1; then echo "::error::Expected exit code 1 for unknown service" exit 1 fi - name: Cleanup credentials if: always() run: rm -f /tmp/credentials.json