feat: V1.0 (#734)
Co-authored-by: Michael Neale <michael.neale@gmail.com> Co-authored-by: Wendy Tang <wendytang@squareup.com> Co-authored-by: Jarrod Sibbison <72240382+jsibbison-square@users.noreply.github.com> Co-authored-by: Alex Hancock <alex.hancock@example.com> Co-authored-by: Alex Hancock <alexhancock@block.xyz> Co-authored-by: Lifei Zhou <lifei@squareup.com> Co-authored-by: Wes <141185334+wesrblock@users.noreply.github.com> Co-authored-by: Max Novich <maksymstepanenko1990@gmail.com> Co-authored-by: Zaki Ali <zaki@squareup.com> Co-authored-by: Salman Mohammed <smohammed@squareup.com> Co-authored-by: Kalvin C <kalvinnchau@users.noreply.github.com> Co-authored-by: Alec Thomas <alec@swapoff.org> Co-authored-by: lily-de <119957291+lily-de@users.noreply.github.com> Co-authored-by: kalvinnchau <kalvin@block.xyz> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Rizel Scarlett <rizel@squareup.com> Co-authored-by: bwrage <bwrage@squareup.com> Co-authored-by: Kalvin Chau <kalvin@squareup.com> Co-authored-by: Alice Hau <110418948+ahau-square@users.noreply.github.com> Co-authored-by: Alistair Gray <ajgray@stripe.com> Co-authored-by: Nahiyan Khan <nahiyan.khan@gmail.com> Co-authored-by: Alex Hancock <alexhancock@squareup.com> Co-authored-by: Nahiyan Khan <nahiyan@squareup.com> Co-authored-by: marcelle <1852848+laanak08@users.noreply.github.com> Co-authored-by: Yingjie He <yingjiehe@block.xyz> Co-authored-by: Yingjie He <yingjiehe@squareup.com> Co-authored-by: Lily Delalande <ldelalande@block.xyz> Co-authored-by: Adewale Abati <acekyd01@gmail.com> Co-authored-by: Ebony Louis <ebony774@gmail.com> Co-authored-by: Angie Jones <jones.angie@gmail.com> Co-authored-by: Ebony Louis <55366651+EbonyLouis@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
# This is a **reuseable** workflow that bundles the Desktop App for macOS.
|
||||
# It doesn't get triggered on its own. It gets used in multiple workflows:
|
||||
# - release.yml
|
||||
# - canary.yml
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
# Let's allow overriding the OSes and architectures in JSON array form:
|
||||
# e.g. '["ubuntu-latest","macos-latest"]'
|
||||
# If no input is provided, these defaults apply.
|
||||
operating-systems:
|
||||
type: string
|
||||
required: false
|
||||
default: '["ubuntu-latest","macos-latest"]'
|
||||
architectures:
|
||||
type: string
|
||||
required: false
|
||||
default: '["x86_64","aarch64"]'
|
||||
|
||||
name: "Reusable workflow to build CLI"
|
||||
|
||||
jobs:
|
||||
build-cli:
|
||||
name: Build CLI
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ${{ fromJson(inputs.operating-systems) }}
|
||||
architecture: ${{ fromJson(inputs.architectures) }}
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target-suffix: unknown-linux-gnu
|
||||
- os: macos-latest
|
||||
target-suffix: apple-darwin
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
target: ${{ matrix.architecture }}-${{ matrix.target-suffix }}
|
||||
|
||||
- name: Install cross
|
||||
run: cargo install cross --git https://github.com/cross-rs/cross
|
||||
|
||||
- name: Build CLI
|
||||
env:
|
||||
CROSS_NO_WARNINGS: 0
|
||||
run: |
|
||||
export TARGET="${{ matrix.architecture }}-${{ matrix.target-suffix }}"
|
||||
rustup target add "${TARGET}"
|
||||
|
||||
# 'cross' is used to cross-compile for different architectures (see Cross.toml)
|
||||
cross build --release --target ${TARGET} -p goose-cli
|
||||
|
||||
# tar the goose binary as goose-<TARGET>.tar.bz2
|
||||
cd target/${TARGET}/release
|
||||
tar -cjf goose-${TARGET}.tar.bz2 goose
|
||||
echo "ARTIFACT=target/${TARGET}/release/goose-${TARGET}.tar.bz2" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload CLI artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: goose-${{ matrix.architecture }}-${{ matrix.target-suffix }}
|
||||
path: ${{ env.ARTIFACT }}
|
||||
@@ -0,0 +1,198 @@
|
||||
# This is a **reuseable** workflow that bundles the Desktop App for macOS.
|
||||
# It doesn't get triggered on its own. It gets used in multiple workflows:
|
||||
# - release.yml
|
||||
# - canary.yml
|
||||
# - pr-comment-bundle-desktop.yml
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
signing:
|
||||
description: 'Whether to perform signing and notarization'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
secrets:
|
||||
CERTIFICATE_OSX_APPLICATION:
|
||||
description: 'Certificate for macOS application signing'
|
||||
required: false
|
||||
CERTIFICATE_PASSWORD:
|
||||
description: 'Password for the macOS certificate'
|
||||
required: false
|
||||
APPLE_ID:
|
||||
description: 'Apple ID for notarization'
|
||||
required: false
|
||||
APPLE_ID_PASSWORD:
|
||||
description: 'Password for the Apple ID'
|
||||
required: false
|
||||
APPLE_TEAM_ID:
|
||||
description: 'Apple Team ID'
|
||||
required: false
|
||||
|
||||
name: Reusable workflow to bundle desktop app
|
||||
|
||||
jobs:
|
||||
bundle-desktop:
|
||||
runs-on: macos-latest
|
||||
name: Bundle Desktop App on macOS
|
||||
steps:
|
||||
# Validate Signing Secrets if signing is enabled
|
||||
- name: Validate Signing Secrets
|
||||
if: ${{ inputs.signing }}
|
||||
run: |
|
||||
if [[ -z "${{ secrets.CERTIFICATE_OSX_APPLICATION }}" ]]; then
|
||||
echo "Error: CERTIFICATE_OSX_APPLICATION secret is required for signing."
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "${{ secrets.CERTIFICATE_PASSWORD }}" ]]; then
|
||||
echo "Error: CERTIFICATE_PASSWORD secret is required for signing."
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "${{ secrets.APPLE_ID }}" ]]; then
|
||||
echo "Error: APPLE_ID secret is required for signing."
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "${{ secrets.APPLE_ID_PASSWORD }}" ]]; then
|
||||
echo "Error: APPLE_ID_PASSWORD secret is required for signing."
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "${{ secrets.APPLE_TEAM_ID }}" ]]; then
|
||||
echo "Error: APPLE_TEAM_ID secret is required for signing."
|
||||
exit 1
|
||||
fi
|
||||
echo "All required signing secrets are present."
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Cache Cargo registry
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cargo/registry
|
||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-registry-
|
||||
|
||||
- name: Cache Cargo index
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cargo/index
|
||||
key: ${{ runner.os }}-cargo-index
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-index
|
||||
|
||||
- name: Cache Cargo build
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: target
|
||||
key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-build-
|
||||
|
||||
- name: Build goosed
|
||||
run: cargo build --release -p goose-server
|
||||
|
||||
- name: Copy binary into Electron folder
|
||||
run: cp target/release/goosed ui/desktop/src/bin/goosed
|
||||
|
||||
# Conditional Signing Step - we skip this for faster builds
|
||||
- name: Add MacOS certs for signing and notarization
|
||||
if: ${{ inputs.signing }}
|
||||
run: ./add-macos-cert.sh
|
||||
working-directory: ui/desktop
|
||||
env:
|
||||
CERTIFICATE_OSX_APPLICATION: ${{ secrets.CERTIFICATE_OSX_APPLICATION }}
|
||||
CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }}
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
working-directory: ui/desktop
|
||||
|
||||
- name: Make Unsigned App
|
||||
if: ${{ !inputs.signing }}
|
||||
run: |
|
||||
attempt=0
|
||||
max_attempts=2
|
||||
until [ $attempt -ge $max_attempts ]; do
|
||||
npm run bundle:default && break
|
||||
attempt=$((attempt + 1))
|
||||
echo "Attempt $attempt failed. Retrying..."
|
||||
sleep 5
|
||||
done
|
||||
if [ $attempt -ge $max_attempts ]; then
|
||||
echo "Action failed after $max_attempts attempts."
|
||||
exit 1
|
||||
fi
|
||||
working-directory: ui/desktop
|
||||
|
||||
- name: Make Signed App
|
||||
if: ${{ inputs.signing }}
|
||||
run: |
|
||||
attempt=0
|
||||
max_attempts=2
|
||||
until [ $attempt -ge $max_attempts ]; do
|
||||
npm run bundle:default && break
|
||||
attempt=$((attempt + 1))
|
||||
echo "Attempt $attempt failed. Retrying..."
|
||||
sleep 5
|
||||
done
|
||||
if [ $attempt -ge $max_attempts ]; then
|
||||
echo "Action failed after $max_attempts attempts."
|
||||
exit 1
|
||||
fi
|
||||
working-directory: ui/desktop
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
|
||||
- name: Upload Desktop artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Goose-darwin-arm64
|
||||
path: ui/desktop/out/Goose-darwin-arm64/Goose.zip
|
||||
|
||||
- name: Quick launch test (macOS)
|
||||
run: |
|
||||
# Ensure no quarantine attributes (if needed)
|
||||
xattr -cr "ui/desktop/out/Goose-darwin-arm64/Goose.app"
|
||||
echo "Opening Goose.app..."
|
||||
open -g "ui/desktop/out/Goose-darwin-arm64/Goose.app"
|
||||
|
||||
# Give the app a few seconds to start and write logs
|
||||
sleep 5
|
||||
|
||||
# Check if it's running
|
||||
if pgrep -f "Goose.app/Contents/MacOS/Goose" > /dev/null; then
|
||||
echo "App appears to be running."
|
||||
else
|
||||
echo "App did not stay open. Possible crash or startup error."
|
||||
exit 1
|
||||
fi
|
||||
LOGFILE="$HOME/Library/Application Support/Goose/logs/main.log"
|
||||
# Print the log and verify "ChatWindow loaded" is in the logs
|
||||
if [ -f "$LOGFILE" ]; then
|
||||
echo "===== Log file contents ====="
|
||||
cat "$LOGFILE"
|
||||
echo "============================="
|
||||
if grep -F "ChatWindow loaded" "$LOGFILE"; then
|
||||
echo "Confirmed: 'ChatWindow loaded' found in logs!"
|
||||
else
|
||||
echo "Did not find 'ChatWindow loaded' in logs. Failing..."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "No log file found at $LOGFILE. Exiting with failure."
|
||||
exit 1
|
||||
fi
|
||||
# Kill the app to clean up
|
||||
pkill -f "Goose.app/Contents/MacOS/Goose"
|
||||
@@ -0,0 +1,82 @@
|
||||
# This workflow is for canary releases, automatically triggered by push to v1.0 branch.
|
||||
# This workflow is identical to "release.yml" with these exceptions:
|
||||
# - Triggered by push to v1.0 branch
|
||||
# - Github Release tagged as "canary"
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
branches:
|
||||
- main
|
||||
|
||||
name: Canary
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# ------------------------------------
|
||||
# 1) Build CLI for multiple OS/Arch
|
||||
# ------------------------------------
|
||||
build-cli:
|
||||
uses: ./.github/workflows/build-cli.yml
|
||||
|
||||
# ------------------------------------
|
||||
# 2) Upload Install CLI Script (we only need to do this once)
|
||||
# ------------------------------------
|
||||
install-script:
|
||||
name: Upload Install Script
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ build-cli ]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: download_cli.sh
|
||||
path: download_cli.sh
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 3) Bundle Desktop App (macOS only) - builds goosed and Electron app
|
||||
# ------------------------------------------------------------
|
||||
bundle-desktop:
|
||||
uses: ./.github/workflows/bundle-desktop.yml
|
||||
with:
|
||||
signing: true
|
||||
secrets:
|
||||
CERTIFICATE_OSX_APPLICATION: ${{ secrets.CERTIFICATE_OSX_APPLICATION }}
|
||||
CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
|
||||
# ------------------------------------
|
||||
# 4) Create/Update GitHub Release
|
||||
# ------------------------------------
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ build-cli, install-script, bundle-desktop ]
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
merge-multiple: true
|
||||
|
||||
# Create/update the canary release
|
||||
- name: Release canary
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
tag: canary
|
||||
name: Canary
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
artifacts: |
|
||||
goose-*.tar.bz2
|
||||
Goose*.zip
|
||||
download_cli.sh
|
||||
allowUpdates: true
|
||||
omitBody: true
|
||||
omitPrereleaseDuringUpdate: true
|
||||
@@ -1,111 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main # Trigger CI on PRs to main
|
||||
|
||||
push:
|
||||
branches:
|
||||
- main # Trigger CI on pushes to main
|
||||
|
||||
jobs:
|
||||
exchange:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
with:
|
||||
version: "0.5.4"
|
||||
|
||||
- name: Source Cargo Environment
|
||||
run: source $HOME/.cargo/env
|
||||
|
||||
- name: Ruff
|
||||
run: |
|
||||
uvx ruff check packages/exchange
|
||||
uvx ruff format packages/exchange --check
|
||||
|
||||
- name: Run tests
|
||||
working-directory: ./packages/exchange
|
||||
run: |
|
||||
uv run pytest tests -m 'not integration'
|
||||
|
||||
goose:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
with:
|
||||
version: "0.5.4"
|
||||
|
||||
- name: Source Cargo Environment
|
||||
run: source $HOME/.cargo/env
|
||||
|
||||
- name: Ruff
|
||||
run: |
|
||||
uvx ruff check src tests
|
||||
uvx ruff format src tests --check
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
uv run pytest tests -m 'not integration'
|
||||
|
||||
|
||||
# This runs integration tests of the OpenAI API, using Ollama to host models.
|
||||
# This lets us test PRs from forks which can't access secrets like API keys.
|
||||
ollama:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
# Only test the lastest python version.
|
||||
- "3.12"
|
||||
ollama-model:
|
||||
# For quicker CI, use a smaller, tool-capable model than the default.
|
||||
- "qwen2.5:0.5b"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
with:
|
||||
version: "0.5.4"
|
||||
|
||||
- name: Source Cargo Environment
|
||||
run: source $HOME/.cargo/env
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install ${{ matrix.python-version }}
|
||||
|
||||
- name: Install Ollama
|
||||
run: curl -fsSL https://ollama.com/install.sh | sh
|
||||
|
||||
- name: Start Ollama
|
||||
run: |
|
||||
# Run the background, in a way that survives to the next step
|
||||
nohup ollama serve > ollama.log 2>&1 &
|
||||
|
||||
# Block using the ready endpoint
|
||||
time curl --retry 5 --retry-connrefused --retry-delay 1 -sf http://localhost:11434
|
||||
|
||||
# Tests use OpenAI which does not have a mechanism to pull models. Run a
|
||||
# simple prompt to (pull and) test the model first.
|
||||
- name: Test Ollama model
|
||||
run: ollama run $OLLAMA_MODEL hello || cat ollama.log
|
||||
env:
|
||||
OLLAMA_MODEL: ${{ matrix.ollama-model }}
|
||||
|
||||
- name: Run Ollama tests
|
||||
run: uv run pytest tests -m integration -k ollama
|
||||
working-directory: ./packages/exchange
|
||||
env:
|
||||
OLLAMA_MODEL: ${{ matrix.ollama-model }}
|
||||
@@ -0,0 +1,107 @@
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
name: CI
|
||||
|
||||
jobs:
|
||||
rust-format:
|
||||
name: Check Rust Code Format
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Run cargo fmt
|
||||
run: cargo fmt --check
|
||||
|
||||
rust-build-and-test:
|
||||
name: Build and Test Rust Project
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt update -y
|
||||
sudo apt install -y libdbus-1-dev gnome-keyring libxcb1-dev
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Cache Cargo Registry
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cargo/registry
|
||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-registry-
|
||||
|
||||
- name: Cache Cargo Index
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cargo/index
|
||||
key: ${{ runner.os }}-cargo-index
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-index
|
||||
|
||||
- name: Cache Cargo Build
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: target
|
||||
key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-build-
|
||||
|
||||
- name: Build and Test
|
||||
run: |
|
||||
gnome-keyring-daemon --components=secrets --daemonize --unlock <<< 'foobar'
|
||||
cargo test
|
||||
working-directory: crates
|
||||
|
||||
- name: Lint
|
||||
run: cargo clippy -- -D warnings
|
||||
|
||||
desktop-lint:
|
||||
name: Lint Electron Desktop App
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
working-directory: ui/desktop
|
||||
|
||||
- name: Run Lint
|
||||
run: npm run lint:check
|
||||
working-directory: ui/desktop
|
||||
|
||||
# Faster Desktop App build for PRs only
|
||||
bundle-desktop-unsigned:
|
||||
uses: ./.github/workflows/bundle-desktop.yml
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
signing: false
|
||||
@@ -0,0 +1,47 @@
|
||||
name: Deploy v1 Docs & Extensions # (/documentation and /extensions-site)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the branch
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install dependencies and build docs
|
||||
working-directory: ./documentation
|
||||
run: |
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
- name: Install dependencies and build extensions-site
|
||||
working-directory: ./extensions-site
|
||||
env:
|
||||
VITE_BASENAME: "/goose/v1/extensions/" # Set the base URL here for the extensions site
|
||||
run: |
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
- name: Combine builds into one directory
|
||||
run: |
|
||||
mkdir combined-build
|
||||
cp -r documentation/build/* combined-build/
|
||||
mkdir -p combined-build/extensions
|
||||
cp -r extensions-site/build/client/* combined-build/extensions/
|
||||
|
||||
- name: Deploy to gh-pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: combined-build
|
||||
destination_dir: v1 # Deploy the site to the 'v1' subfolder
|
||||
@@ -1,40 +0,0 @@
|
||||
name: Deploy MkDocs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main # Trigger deployment on pushes to main
|
||||
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'mkdocs.yml'
|
||||
- '.github/workflows/deploy_docs.yaml'
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'mkdocs.yml'
|
||||
- '.github/workflows/deploy_docs.yaml'
|
||||
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install UV
|
||||
uses: astral-sh/setup-uv@v3
|
||||
|
||||
- name: Create UV virtual environment
|
||||
run: uv venv
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv pip install "mkdocs-material[imaging]" Pillow cairosvg
|
||||
|
||||
- name: Build the documentation
|
||||
run: uv run mkdocs gh-deploy --force
|
||||
@@ -1,53 +0,0 @@
|
||||
---
|
||||
name: License Check
|
||||
|
||||
on:
|
||||
pull_request: # Trigger license check on any PRs
|
||||
paths:
|
||||
- '**/pyproject.toml'
|
||||
- '.github/workflows/license-check.yml'
|
||||
- '.github/workflows/scripts/check_licenses.py'
|
||||
|
||||
push: # Trigger license check on pushes to main
|
||||
branches:
|
||||
- main
|
||||
paths: # TODO: can't DRY unless https://github.com/actions/runner/issues/1182
|
||||
- '**/pyproject.toml'
|
||||
- '.github/workflows/license-check.yml'
|
||||
- '.github/workflows/scripts/check_licenses.py'
|
||||
|
||||
jobs:
|
||||
check-licenses:
|
||||
name: Check Package Licenses
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install tomli requests urllib3
|
||||
|
||||
- name: Check licenses
|
||||
run: |
|
||||
python .github/workflows/scripts/check_licenses.py \
|
||||
pyproject.toml || exit_code=$?
|
||||
if [ "${exit_code:-0}" -ne 0 ]; then
|
||||
echo "::error::Found packages with disallowed licenses"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Check Exchange licenses
|
||||
run: |
|
||||
python .github/workflows/scripts/check_licenses.py \
|
||||
packages/exchange/pyproject.toml || exit_code=$?
|
||||
if [ "${exit_code:-0}" -ne 0 ]; then
|
||||
echo "::error::Found packages with disallowed licenses in exchange"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,76 @@
|
||||
# This workflow is triggered by a comment on an issue or PR with the text ".bundle"
|
||||
# It bundles the Desktop App, then creates a PR comment with a link to download the app.
|
||||
|
||||
# IMPORTANT: issue_comment workflows only use files found on "main" branch to run.
|
||||
# Do NOT allow on: pull_request since that allows a user to alter a file in a PR and exfil secrets for example.
|
||||
# Only using issue_comment is the suggested workflow type. Comments on pull requests, and issues will trigger the issue_comment workflow event.
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
# permissions needed for reacting to IssueOps commands on PRs
|
||||
permissions:
|
||||
pull-requests: write
|
||||
checks: read
|
||||
# issues: write
|
||||
|
||||
name: Workflow to Bundle Desktop App
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
trigger-on-command:
|
||||
name: Trigger on ".bundle" PR comment
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: github/command@v1.3.0
|
||||
id: command
|
||||
with:
|
||||
command: ".bundle"
|
||||
reaction: "eyes"
|
||||
allowed_contexts: pull_request
|
||||
|
||||
bundle-desktop:
|
||||
# Only run this if ".bundle" command is detected.
|
||||
if: ${{ steps.command.outputs.continue == 'true' }}
|
||||
uses: ./.github/workflows/bundle-desktop.yml
|
||||
with:
|
||||
signing: true
|
||||
secrets:
|
||||
CERTIFICATE_OSX_APPLICATION: ${{ secrets.CERTIFICATE_OSX_APPLICATION }}
|
||||
CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
|
||||
pr-comment:
|
||||
name: PR Comment with Desktop App
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ bundle-desktop ]
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Comment on PR with download link
|
||||
uses: peter-evans/create-or-update-comment@v3
|
||||
with:
|
||||
comment-id: ${{ steps.command.outputs.comment_id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body: |
|
||||
### Desktop App for this PR
|
||||
|
||||
The following build is available for testing:
|
||||
|
||||
- [📱 macOS Desktop App (arm64, signed)](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/Goose-darwin-arm64.zip)
|
||||
|
||||
After downloading, unzip the file and drag the Goose.app to your Applications folder. The app is signed and notarized for macOS.
|
||||
|
||||
This link is provided by nightly.link and will work even if you're not logged into GitHub.
|
||||
edit-mode: replace
|
||||
@@ -1,50 +0,0 @@
|
||||
name: Publish
|
||||
|
||||
# A release on goose will also publish exchange, if it has updated
|
||||
# This means in some cases we may need to make a bump in goose without other changes to release exchange
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get current version from pyproject.toml
|
||||
id: get_version
|
||||
run: |
|
||||
echo "VERSION=$(grep -m 1 'version =' "pyproject.toml" | awk -F'"' '{print $2}')" >> $GITHUB_ENV
|
||||
|
||||
- name: Extract tag version
|
||||
id: extract_tag
|
||||
run: |
|
||||
TAG_VERSION=$(echo "${{ github.event.release.tag_name }}" | sed -E 's/v(.*)/\1/')
|
||||
echo "TAG_VERSION=$TAG_VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Check if tag matches version from pyproject.toml
|
||||
id: check_tag
|
||||
run: |
|
||||
if [ "${{ env.TAG_VERSION }}" != "${{ env.VERSION }}" ]; then
|
||||
echo "::error::Tag version (${{ env.TAG_VERSION }}) does not match version in pyproject.toml (${{ env.VERSION }})."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@v1
|
||||
with:
|
||||
version: "latest"
|
||||
|
||||
- name: Build Package
|
||||
run: |
|
||||
uv build -o dist --package goose-ai
|
||||
uv build -o dist --package ai-exchange
|
||||
|
||||
- name: Publish package to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
skip-existing: true
|
||||
@@ -1,48 +0,0 @@
|
||||
name: 'Lint PR'
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- synchronize
|
||||
- reopened
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
main:
|
||||
name: Validate PR title
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@v5
|
||||
id: lint_pr_title
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
requireScope: false
|
||||
|
||||
- uses: marocchino/sticky-pull-request-comment@v2
|
||||
# When the previous steps fails, the workflow would stop. By adding this
|
||||
# condition you can continue the execution with the populated error message.
|
||||
if: always() && (steps.lint_pr_title.outputs.error_message != null)
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
message: |
|
||||
Hey there and thank you for opening this pull request! 👋🏼
|
||||
|
||||
We require pull request titles to follow the [Conventional Commits specification](https://gist.github.com/Zekfad/f51cb06ac76e2457f11c80ed705c95a3#file-conventional-commits-md) and it looks like your proposed title needs to be adjusted.
|
||||
|
||||
Details:
|
||||
|
||||
```
|
||||
${{ steps.lint_pr_title.outputs.error_message }}
|
||||
```
|
||||
|
||||
# Delete a previous comment when the issue has been resolved
|
||||
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
delete: true
|
||||
@@ -1,41 +0,0 @@
|
||||
name: Release Monitor
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch: # Add this line to enable manual triggering
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pipx
|
||||
pipx install goose-ai
|
||||
|
||||
- name: Check Goose AI Version
|
||||
run: goose version
|
||||
|
||||
- name: Create Issue on Failure
|
||||
if: failure()
|
||||
uses: actions/github-script@v3
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
await github.issues.create({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
title: 'Release Build Failed',
|
||||
body: `The release for version ${{ github.event.release.tag_name }} failed to run. Please investigate the issue.`
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
# This workflow is main release, needs to be manually tagged & pushed.
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v1.*"
|
||||
|
||||
name: Release
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# ------------------------------------
|
||||
# 1) Build CLI for multiple OS/Arch
|
||||
# ------------------------------------
|
||||
build-cli:
|
||||
uses: ./.github/workflows/build-cli.yml
|
||||
|
||||
# ------------------------------------
|
||||
# 2) Upload Install CLI Script (we only need to do this once)
|
||||
# ------------------------------------
|
||||
install-script:
|
||||
name: Upload Install Script
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ build-cli ]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: download_cli.sh
|
||||
path: download_cli.sh
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 3) Bundle Desktop App (macOS only) - builds goosed and Electron app
|
||||
# ------------------------------------------------------------
|
||||
bundle-desktop:
|
||||
uses: ./.github/workflows/bundle-desktop.yml
|
||||
with:
|
||||
signing: true
|
||||
secrets:
|
||||
CERTIFICATE_OSX_APPLICATION: ${{ secrets.CERTIFICATE_OSX_APPLICATION }}
|
||||
CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
|
||||
# ------------------------------------
|
||||
# 4) Create/Update GitHub Release
|
||||
# ------------------------------------
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ build-cli, install-script, bundle-desktop ]
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
merge-multiple: true
|
||||
|
||||
# Create/update the versioned release
|
||||
- name: Release versioned
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# This pattern will match both goose tar.bz2 artifacts and the Goose.zip
|
||||
artifacts: |
|
||||
goose-*.tar.bz2
|
||||
Goose*.zip
|
||||
download_cli.sh
|
||||
allowUpdates: true
|
||||
omitBody: true
|
||||
omitPrereleaseDuringUpdate: true
|
||||
|
||||
# Create/update the stable release
|
||||
- name: Release stable
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
tag: stable
|
||||
name: Stable
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
artifacts: |
|
||||
goose-*.tar.bz2
|
||||
Goose*.zip
|
||||
download_cli.sh
|
||||
allowUpdates: true
|
||||
omitBody: true
|
||||
omitPrereleaseDuringUpdate: true
|
||||
@@ -1,320 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
import tomli
|
||||
import urllib3
|
||||
|
||||
|
||||
class Color(str, Enum):
|
||||
"""ANSI color codes with fallback for non-color terminals"""
|
||||
|
||||
@staticmethod
|
||||
def supports_color() -> bool:
|
||||
"""Check if the terminal supports color output."""
|
||||
if not hasattr(sys.stdout, "isatty"):
|
||||
return False
|
||||
if not sys.stdout.isatty():
|
||||
return False
|
||||
|
||||
if "NO_COLOR" in os.environ:
|
||||
return False
|
||||
|
||||
term = os.environ.get("TERM", "")
|
||||
if term == "dumb":
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
has_color = supports_color()
|
||||
|
||||
RED = "\033[91m" if has_color else ""
|
||||
GREEN = "\033[92m" if has_color else ""
|
||||
RESET = "\033[0m" if has_color else ""
|
||||
BOLD = "\033[1m" if has_color else ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LicenseConfig:
|
||||
allowed_licenses: frozenset[str] = frozenset(
|
||||
{
|
||||
"MIT",
|
||||
"BSD-3-Clause",
|
||||
"Apache-2.0",
|
||||
"Apache License 2",
|
||||
"Apache Software License",
|
||||
"Python Software Foundation License",
|
||||
"BSD License",
|
||||
"ISC",
|
||||
}
|
||||
)
|
||||
exceptions: frozenset[str] = frozenset(
|
||||
{
|
||||
"ai-exchange",
|
||||
"tiktoken",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LicenseInfo:
|
||||
license: str | None
|
||||
allowed: bool = False
|
||||
|
||||
def __str__(self) -> str:
|
||||
status = "✓" if self.allowed else "✗"
|
||||
color = Color.GREEN if self.allowed else Color.RED
|
||||
return f"{color}{status}{Color.RESET} {self.license}"
|
||||
|
||||
|
||||
class LicenseChecker:
|
||||
def __init__(self, config: LicenseConfig = LicenseConfig()) -> None:
|
||||
self.config = config
|
||||
self.session = self._setup_session()
|
||||
|
||||
def _setup_session(self) -> requests.Session:
|
||||
session = requests.Session()
|
||||
session.verify = True
|
||||
max_retries = urllib3.util.Retry(
|
||||
total=3,
|
||||
backoff_factor=0.5,
|
||||
status_forcelist=[
|
||||
500,
|
||||
502,
|
||||
503,
|
||||
504,
|
||||
],
|
||||
)
|
||||
adapter = requests.adapters.HTTPAdapter(max_retries=max_retries)
|
||||
session.mount("https://", adapter)
|
||||
return session
|
||||
|
||||
def normalize_license(self, license_str: str | None) -> str | None:
|
||||
"""
|
||||
Normalize license string for comparison.
|
||||
|
||||
This method takes a license string and normalizes it by:
|
||||
1. Converting to uppercase
|
||||
2. Removing 'LICENSE' or 'LICENCE' suffixes
|
||||
3. Stripping whitespace
|
||||
4. Replacing common variations with standardized forms
|
||||
|
||||
Args:
|
||||
license_str (str | None): The original license string to normalize.
|
||||
|
||||
Returns:
|
||||
str | None: The normalized license string, or None if the input was None.
|
||||
"""
|
||||
if not license_str:
|
||||
return None
|
||||
|
||||
# fmt: off
|
||||
normalized = (
|
||||
license_str.upper()
|
||||
.replace(" LICENSE", "")
|
||||
.replace(" LICENCE", "")
|
||||
.strip()
|
||||
)
|
||||
# fmt: on
|
||||
|
||||
replacements = {
|
||||
"APACHE 2.0": "APACHE-2.0",
|
||||
"APACHE SOFTWARE LICENSE": "APACHE-2.0",
|
||||
"BSD": "BSD-3-CLAUSE",
|
||||
"MIT LICENSE": "MIT",
|
||||
"PYTHON SOFTWARE FOUNDATION": "PSF",
|
||||
}
|
||||
|
||||
return replacements.get(normalized, normalized)
|
||||
|
||||
def get_package_license(self, package_name: str) -> str | None:
|
||||
"""Fetch license information from PyPI.
|
||||
|
||||
Args:
|
||||
package_name (str): The name of the package to fetch the license for.
|
||||
|
||||
Returns:
|
||||
str | None: The license of the package, or None if not found.
|
||||
"""
|
||||
try:
|
||||
response = self.session.get(
|
||||
f"https://pypi.org/pypi/{package_name}/json",
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
# fmt: off
|
||||
license_info = (
|
||||
data["info"].get("license") or
|
||||
data["info"].get("classifiers", [])
|
||||
)
|
||||
# fmt: on
|
||||
|
||||
if isinstance(license_info, list):
|
||||
for classifier in license_info:
|
||||
if classifier.startswith("License :: "):
|
||||
parts = classifier.split(" :: ")
|
||||
return parts[-1]
|
||||
|
||||
return license_info if isinstance(license_info, str) else None
|
||||
|
||||
except requests.exceptions.SSLError as e:
|
||||
print(f"SSL Error fetching license for {package_name}: {e}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not fetch license for {package_name}: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
def extract_dependencies(self, toml_file: Path) -> list[str]:
|
||||
"""Extract all dependencies from a TOML file."""
|
||||
with open(toml_file, "rb") as f:
|
||||
data = tomli.load(f)
|
||||
|
||||
dependencies = []
|
||||
|
||||
# Get direct dependencies
|
||||
project_deps = data.get("project", {}).get("dependencies", [])
|
||||
dependencies.extend(self._parse_dependency_strings(project_deps))
|
||||
|
||||
# Get dev dependencies
|
||||
tool_deps = data.get("tool", {}).get("uv", {}).get("dev-dependencies", [])
|
||||
dependencies.extend(self._parse_dependency_strings(tool_deps))
|
||||
|
||||
return list(set(dependencies))
|
||||
|
||||
def _parse_dependency_strings(self, deps: list[str]) -> list[str]:
|
||||
"""
|
||||
Parse dependency strings to extract package names.
|
||||
|
||||
Args:
|
||||
deps (list[str]): A list of dependency strings to parse.
|
||||
|
||||
Returns:
|
||||
list[str]: A list of extracted package names.
|
||||
"""
|
||||
packages = []
|
||||
for dep in deps:
|
||||
if "workspace = true" in dep:
|
||||
continue
|
||||
|
||||
# fmt: off
|
||||
# Handle basic package specifiers
|
||||
package = (
|
||||
dep.split(">=")[0]
|
||||
.split("==")[0]
|
||||
.split("<")[0]
|
||||
.split(">")[0]
|
||||
.strip()
|
||||
)
|
||||
package = package.split("{")[0].strip()
|
||||
# fmt: on
|
||||
if package:
|
||||
packages.append(package)
|
||||
return packages
|
||||
|
||||
def check_licenses(self, toml_file: Path) -> dict[str, LicenseInfo]:
|
||||
"""
|
||||
Check licenses for all dependencies in the TOML file.
|
||||
|
||||
Args:
|
||||
toml_file (Path): The path to the TOML file containing the dependencies.
|
||||
|
||||
Returns:
|
||||
dict[str, LicenseInfo]: A dictionary where the keys are package names and the values are LicenseInfo objects
|
||||
containing the license information and whether it's allowed."""
|
||||
dependencies = self.extract_dependencies(toml_file)
|
||||
results: dict[str, LicenseInfo] = {}
|
||||
checked: set[str] = set()
|
||||
|
||||
for package in dependencies:
|
||||
if package in checked:
|
||||
continue
|
||||
|
||||
checked.add(package)
|
||||
results[package] = self._check_package(package)
|
||||
|
||||
return results
|
||||
|
||||
def _check_package(self, package: str) -> LicenseInfo:
|
||||
"""
|
||||
Check license for a single package.
|
||||
|
||||
Args:
|
||||
package (str): The name of the package to check.
|
||||
|
||||
Returns:
|
||||
LicenseInfo: A LicenseInfo object containing the license
|
||||
information and whether it's allowed.
|
||||
"""
|
||||
|
||||
if package in self.config.exceptions:
|
||||
return LicenseInfo("Approved Exception", True)
|
||||
|
||||
license_info = self.get_package_license(package)
|
||||
normalized_license = self.normalize_license(license_info)
|
||||
allowed = False
|
||||
|
||||
# fmt: off
|
||||
if normalized_license:
|
||||
allowed = normalized_license in {
|
||||
self.normalize_license(x)
|
||||
for x in self.config.allowed_licenses
|
||||
}
|
||||
# fmt: on
|
||||
return LicenseInfo(license_info, allowed)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Check package licenses in TOML files")
|
||||
parser.add_argument("toml_files", type=Path, nargs="*", help="Paths to TOML files")
|
||||
parser.add_argument("--supported-licenses", action="store_true", help="Print supported licenses")
|
||||
|
||||
checker = LicenseChecker()
|
||||
all_results: dict[str, LicenseInfo] = {}
|
||||
|
||||
args = parser.parse_args()
|
||||
if args.supported_licenses:
|
||||
for license in sorted(checker.config.allowed_licenses, key=str.casefold):
|
||||
print(f" - {license}")
|
||||
sys.exit(0)
|
||||
|
||||
if not args.toml_files:
|
||||
print("Error: No TOML files specified", file=sys.stderr)
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
for toml_file in args.toml_files:
|
||||
results = checker.check_licenses(toml_file)
|
||||
for package, info in results.items():
|
||||
if package in all_results and all_results[package] != info:
|
||||
print(f"Warning: Package {package} has conflicting license info:", file=sys.stderr)
|
||||
print(f" {toml_file}: {info}", file=sys.stderr)
|
||||
print(f" Previous: {all_results[package]}", file=sys.stderr)
|
||||
all_results[package] = info
|
||||
|
||||
max_package_length = max(len(package) for package in all_results.keys())
|
||||
any_disallowed = False
|
||||
|
||||
for package, info in sorted(all_results.items()):
|
||||
if Color.has_color:
|
||||
package_name = f"{Color.BOLD}{package}{Color.RESET}"
|
||||
padding = len(Color.BOLD) + len(Color.RESET)
|
||||
else:
|
||||
package_name = package
|
||||
padding = 0
|
||||
|
||||
print(f"{package_name:<{max_package_length + padding}} {info}")
|
||||
if not info.allowed:
|
||||
any_disallowed = True
|
||||
|
||||
sys.exit(1 if any_disallowed else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,248 +0,0 @@
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
import tomli
|
||||
from check_licenses import Color, LicenseChecker, LicenseConfig, LicenseInfo, main
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def checker() -> LicenseChecker:
|
||||
return LicenseChecker()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_pypi_response() -> Mock:
|
||||
response = Mock()
|
||||
response.status_code = 200
|
||||
response.raise_for_status = Mock()
|
||||
response.ok = True
|
||||
response.json.return_value = {
|
||||
"info": {
|
||||
"license": "Apache-2.0",
|
||||
}
|
||||
}
|
||||
return response
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_toml_content() -> str:
|
||||
return """
|
||||
[project]
|
||||
dependencies = [
|
||||
"requests>=2.28.0",
|
||||
"tomli==2.0.1",
|
||||
"urllib3<2.0.0",
|
||||
"package-with-workspace{workspace = true}",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
dev-dependencies = [
|
||||
"pytest>=7.0.0",
|
||||
"black==23.3.0",
|
||||
]
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_toml_files(tmp_path: Path) -> list[Path]:
|
||||
"""Create mock TOML files with different dependencies."""
|
||||
file1 = tmp_path / "pyproject1.toml"
|
||||
|
||||
file1.write_text("""
|
||||
[project]
|
||||
dependencies = [
|
||||
"requests>=2.28.0",
|
||||
"tomli==2.0.1",
|
||||
]
|
||||
""")
|
||||
|
||||
file2 = tmp_path / "pyproject2.toml"
|
||||
file2.write_text("""
|
||||
[project]
|
||||
dependencies = [
|
||||
"urllib3<2.0.0",
|
||||
"requests>=2.27.0", # Different version but same package
|
||||
]
|
||||
""")
|
||||
|
||||
return [file1, file2]
|
||||
|
||||
|
||||
def test_normalize_license_variations(checker: LicenseChecker) -> None:
|
||||
assert checker.normalize_license("MIT License") == "MIT"
|
||||
assert checker.normalize_license("Apache 2.0") == "APACHE-2.0"
|
||||
assert checker.normalize_license("BSD") == "BSD-3-CLAUSE"
|
||||
assert checker.normalize_license(None) is None
|
||||
assert checker.normalize_license("") is None
|
||||
assert checker.normalize_license("MIT License") == "MIT"
|
||||
assert checker.normalize_license("Apache 2.0") == "APACHE-2.0"
|
||||
assert checker.normalize_license("BSD") == "BSD-3-CLAUSE"
|
||||
assert checker.normalize_license(None) is None
|
||||
assert checker.normalize_license("") is None
|
||||
|
||||
|
||||
@patch.object(LicenseChecker, "get_package_license")
|
||||
def test_package_license_verification(mock_get_license: Mock, checker: LicenseChecker) -> None:
|
||||
def check_package_license(
|
||||
package: str, license: Optional[str], expected_allowed: bool, expected_license: str
|
||||
) -> None:
|
||||
if license:
|
||||
mock_get_license.return_value = license
|
||||
result = checker._check_package(package=package)
|
||||
assert result.allowed is expected_allowed
|
||||
assert result.license == expected_license
|
||||
|
||||
check_package_license(
|
||||
package="tiktoken",
|
||||
license=None,
|
||||
expected_allowed=True,
|
||||
expected_license="Approved Exception",
|
||||
)
|
||||
check_package_license(
|
||||
package="requests",
|
||||
license="Apache-2.0",
|
||||
expected_allowed=True,
|
||||
expected_license="Apache-2.0",
|
||||
)
|
||||
check_package_license(
|
||||
package="gpl-package",
|
||||
license="GPL",
|
||||
expected_allowed=False,
|
||||
expected_license="GPL",
|
||||
)
|
||||
|
||||
|
||||
def test_color_support_with_environment_variables(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def verify_color_support_disabled(*, env_var: str, value: str) -> None:
|
||||
monkeypatch.setenv(env_var, value)
|
||||
assert not Color.supports_color()
|
||||
monkeypatch.undo()
|
||||
|
||||
verify_color_support_disabled(env_var="NO_COLOR", value="1")
|
||||
verify_color_support_disabled(env_var="TERM", value="dumb")
|
||||
|
||||
|
||||
@patch("tomli.load")
|
||||
@patch("builtins.open")
|
||||
def test_extract_dependencies(
|
||||
mock_open: Mock, mock_tomli_load: Mock, checker: LicenseChecker, mock_toml_content: str
|
||||
) -> None:
|
||||
mock_tomli_load.return_value = tomli.loads(mock_toml_content)
|
||||
mock_file = Mock()
|
||||
mock_open.return_value.__enter__.return_value = mock_file
|
||||
|
||||
dependencies = checker.extract_dependencies(Path("mock_pyproject.toml"))
|
||||
expected = ["requests", "tomli", "urllib3", "pytest", "black"]
|
||||
assert sorted(dependencies) == sorted(expected)
|
||||
|
||||
|
||||
@patch("requests.Session")
|
||||
def test_get_package_license(mock_session: Mock, checker: LicenseChecker, mock_pypi_response: Mock) -> None:
|
||||
mock_session.return_value.get.return_value = mock_pypi_response
|
||||
assert checker.get_package_license("requests") == "Apache-2.0"
|
||||
|
||||
# test exception handling
|
||||
mock_session.return_value.get.side_effect = Exception("error")
|
||||
assert checker.get_package_license("nonexistent-package") is None
|
||||
|
||||
|
||||
def test_license_info_string_representation() -> None:
|
||||
def assert_license_info_str(info: LicenseInfo, no_color_expected: str, color_expected: str) -> None:
|
||||
expected = color_expected if Color.has_color else no_color_expected
|
||||
assert str(info) == expected
|
||||
|
||||
assert_license_info_str(LicenseInfo("MIT", True), "✓ MIT", f"{Color.GREEN}✓{Color.RESET} MIT")
|
||||
assert_license_info_str(LicenseInfo("GPL", False), "✗ GPL", f"{Color.RED}✗{Color.RESET} GPL")
|
||||
|
||||
|
||||
def test_custom_license_config() -> None:
|
||||
custom_config = LicenseConfig(
|
||||
allowed_licenses=frozenset({"MIT", "Apache-2.0"}), exceptions=frozenset({"special-package"})
|
||||
)
|
||||
checker = LicenseChecker(config=custom_config)
|
||||
|
||||
assert "special-package" in checker.config.exceptions
|
||||
assert len(checker.config.allowed_licenses) == 2
|
||||
|
||||
|
||||
def test_dependency_parsing_scenarios() -> None:
|
||||
"""Test various dependency parsing scenarios."""
|
||||
|
||||
def parse_dependencies(toml_string: str) -> list[str]:
|
||||
checker = LicenseChecker()
|
||||
return checker._parse_dependency_strings(tomli.loads(toml_string)["project"]["dependencies"])
|
||||
|
||||
# basic version specifiers
|
||||
parsed = parse_dependencies("""
|
||||
[project]
|
||||
dependencies = [
|
||||
"requests>=2.28.0",
|
||||
"tomli==2.0.1",
|
||||
"urllib3<2.0.0",
|
||||
]
|
||||
""")
|
||||
assert sorted(parsed) == sorted(["requests", "tomli", "urllib3"])
|
||||
|
||||
# workspace dependencies
|
||||
parsed = parse_dependencies("""
|
||||
[project]
|
||||
dependencies = [
|
||||
"package-with-workspace{workspace = true}",
|
||||
]
|
||||
""")
|
||||
assert parsed == []
|
||||
|
||||
# mixed dependencies
|
||||
parsed = parse_dependencies("""
|
||||
[project]
|
||||
dependencies = [
|
||||
"requests>=2.28.0",
|
||||
"package-with-workspace{workspace = true}",
|
||||
"urllib3<2.0.0",
|
||||
]
|
||||
""")
|
||||
assert sorted(parsed) == sorted(["requests", "urllib3"])
|
||||
|
||||
# multiple version constraints
|
||||
parsed = parse_dependencies("""
|
||||
[project]
|
||||
dependencies = [
|
||||
"urllib3>=1.25.4,<2.0.0",
|
||||
"requests>=2.28.0,<3.0.0",
|
||||
]
|
||||
""")
|
||||
assert sorted(parsed) == sorted(["urllib3", "requests"])
|
||||
|
||||
# empty dependencies
|
||||
parsed = parse_dependencies("""
|
||||
[project]
|
||||
dependencies = []
|
||||
""")
|
||||
assert parsed == []
|
||||
|
||||
|
||||
@patch.object(LicenseChecker, "get_package_license")
|
||||
def test_multiple_toml_files(
|
||||
mock_get_license: Mock,
|
||||
mock_toml_files: list[Path],
|
||||
capsys: pytest.CaptureFixture,
|
||||
) -> None:
|
||||
def get_license(package: str) -> Optional[str]:
|
||||
licenses = {"requests": "Apache-2.0", "tomli": "MIT", "urllib3": "MIT"}
|
||||
return licenses.get(package)
|
||||
|
||||
mock_get_license.side_effect = get_license
|
||||
|
||||
with patch("sys.argv", ["check_licenses.py"] + [str(f) for f in mock_toml_files]):
|
||||
try:
|
||||
main()
|
||||
except SystemExit as e:
|
||||
assert e.code == 0
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert "requests" in captured.out
|
||||
assert "tomli" in captured.out
|
||||
assert "urllib3" in captured.out
|
||||
assert "✗" not in captured.out
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"pull_request": {
|
||||
"head": {
|
||||
"ref": "test-branch"
|
||||
},
|
||||
"base": {
|
||||
"ref": "main"
|
||||
},
|
||||
"number": 123,
|
||||
"title": "test: Update dependency licenses"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user