fix: scope database health check to current repo to enable concurrent sessions #1116
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: PR Checks | |
| on: | |
| pull_request: | |
| branches: [main] | |
| env: | |
| COVERAGE_THRESHOLD: 51.0 | |
| concurrency: | |
| group: pr-checks-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| statuses: write | |
| jobs: | |
| validate-pr-title: | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - name: Validate PR title | |
| uses: amannn/action-semantic-pull-request@v6 | |
| with: | |
| # NOTE: Keep these types in sync with cz_conventional_commits in pyproject.toml | |
| # Standard Conventional Commits types as used by commitizen | |
| types: | | |
| feat | |
| fix | |
| docs | |
| style | |
| refactor | |
| perf | |
| test | |
| build | |
| ci | |
| chore | |
| revert | |
| requireScope: false | |
| validateSingleCommit: false | |
| ignoreLabels: | | |
| skip-validation | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # Linting and type checking (runs in parallel with tests) | |
| lint-and-typecheck: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '' | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Install dependencies | |
| run: uv sync --all-extras | |
| - name: Run ruff | |
| run: uv run ruff check src/ | |
| - name: Run ruff format check | |
| run: uv run ruff format --check src/ | |
| - name: Run mypy | |
| run: uv run mypy src/ | |
| # Test Group 1: Config, conversation, history, and agents (medium load) | |
| test-group-1: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '' | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Install dependencies | |
| run: uv sync --all-extras | |
| - name: Run tests - Config, History & Agents | |
| run: | | |
| uv run pytest \ | |
| test/unit/test_config*.py \ | |
| test/unit/test_conversation*.py \ | |
| test/unit/test_history*.py \ | |
| test/unit/agents/ \ | |
| --cov=src --cov-report= -q | |
| # Note: test_config*.py includes test_config_manager.py and test_config_migrations.py | |
| - name: Upload coverage data | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: coverage-group-1 | |
| path: .coverage | |
| include-hidden-files: true | |
| # Test Group 2: Codebase, LLM proxy, Web (heavy load - Kuzu database) | |
| test-group-2: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '' | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Install dependencies | |
| run: uv sync --all-extras | |
| - name: Run tests - Codebase, LLM Proxy & Web | |
| run: | | |
| uv run pytest \ | |
| test/unit/codebase/ \ | |
| test/unit/llm_proxy/ \ | |
| test/unit/shotgun_web/ \ | |
| test/unit/test_web_search.py \ | |
| --cov=src --cov-report= -q | |
| - name: Upload coverage data | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: coverage-group-2 | |
| path: .coverage | |
| include-hidden-files: true | |
| # Test Group 3: CLI, TUI, exceptions & utilities (light load) | |
| test-group-3: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '' | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Install dependencies | |
| run: uv sync --all-extras | |
| - name: Run tests - CLI, TUI & Utilities | |
| run: | | |
| uv run pytest \ | |
| test/unit/cli/ \ | |
| test/unit/tui/ \ | |
| test/unit/exceptions/ \ | |
| test/unit/test_common.py \ | |
| test/unit/test_file_management.py \ | |
| test/unit/test_file_system_utils.py \ | |
| test/unit/test_logging_config.py \ | |
| test/unit/test_marketing.py \ | |
| test/unit/test_posthog_telemetry.py \ | |
| test/unit/test_provider.py \ | |
| test/unit/test_settings.py \ | |
| test/unit/test_streaming_detection.py \ | |
| test/unit/test_update_checker.py \ | |
| test/unit/test_agent_manager_filtering.py \ | |
| --cov=src --cov-report= -q | |
| - name: Upload coverage data | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: coverage-group-3 | |
| path: .coverage | |
| include-hidden-files: true | |
| # Combine coverage from all test groups | |
| coverage-report: | |
| runs-on: ubuntu-latest | |
| needs: [test-group-1, test-group-2, test-group-3] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '' | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Install dependencies | |
| run: uv sync --all-extras | |
| - name: Download coverage artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: coverage-group-* | |
| merge-multiple: false | |
| - name: Combine coverage data | |
| run: | | |
| # Move coverage files with unique names | |
| mv coverage-group-1/.coverage .coverage.group1 | |
| mv coverage-group-2/.coverage .coverage.group2 | |
| mv coverage-group-3/.coverage .coverage.group3 | |
| # Combine all coverage data | |
| uv run coverage combine | |
| - name: Generate coverage reports | |
| run: | | |
| uv run coverage report --show-missing | |
| uv run coverage xml | |
| - name: Check core coverage (${{ env.COVERAGE_THRESHOLD }}% minimum, excluding CLI/TUI) | |
| run: | | |
| # Use combined coverage data from all test groups | |
| # excluding CLI and TUI directories | |
| echo "Checking combined coverage for core functionality (excluding CLI/TUI)..." | |
| # Run coverage report with specific omit patterns for CLI/TUI and extract percentage | |
| COVERAGE_LINE=$(uv run coverage report --omit="*/cli/*,*/tui/*" | grep "^TOTAL" | awk '{print $NF}' | sed 's/%//') | |
| COVERAGE_OUTPUT=${COVERAGE_LINE:-0} | |
| echo "Core coverage: ${COVERAGE_OUTPUT}%" | |
| echo "" | |
| echo "Current coverage by file (excluding CLI/TUI):" | |
| uv run coverage report --omit="*/cli/*,*/tui/*" --show-missing | |
| echo "" | |
| # Check if coverage meets threshold using bc for decimal comparison | |
| if (( $(echo "${COVERAGE_OUTPUT} < ${COVERAGE_THRESHOLD}" | bc -l) )); then | |
| echo "❌ Coverage check failed: ${COVERAGE_OUTPUT}% < ${COVERAGE_THRESHOLD}%" | |
| echo "" | |
| echo "Please add tests to improve coverage of core functionality" | |
| echo "Focus on: agents/, config/, tools/, models/, history/ directories" | |
| echo "" | |
| echo "Low coverage files that need attention:" | |
| uv run coverage report --omit="*/cli/*,*/tui/*" --show-missing | grep -E "^\w" | awk -v threshold="${COVERAGE_THRESHOLD}" '$NF ~ /%/ && $NF+0 < threshold {print " - " $1 ": " $NF}' | |
| exit 1 | |
| else | |
| echo "✅ Coverage check passed: ${COVERAGE_OUTPUT}% >= ${COVERAGE_THRESHOLD}%" | |
| fi | |
| test-cli-basics: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.11' | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Create virtual environment | |
| run: uv venv | |
| - name: Install dependencies | |
| run: | | |
| source .venv/bin/activate | |
| uv sync --all-extras | |
| - name: Test shotgun version | |
| run: | | |
| source .venv/bin/activate | |
| shotgun --version | |
| - name: Test shotgun help | |
| run: | | |
| source .venv/bin/activate | |
| shotgun --help | |
| check-permissions: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| is-maintainer: ${{ steps.check.outputs.is-maintainer }} | |
| steps: | |
| - name: Check if user is maintainer | |
| id: check | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| try { | |
| const actor = context.actor; | |
| const { owner, repo } = context.repo; | |
| console.log(`Checking permissions for user: ${actor}`); | |
| // Get the user's permission level for this repository | |
| const { data: permissionData } = await github.rest.repos.getCollaboratorPermissionLevel({ | |
| owner, | |
| repo, | |
| username: actor, | |
| }); | |
| const permission = permissionData.permission; | |
| console.log(`User ${actor} has permission level: ${permission}`); | |
| // Maintainers have 'write', 'maintain', or 'admin' permissions | |
| const isMaintainer = ['write', 'maintain', 'admin'].includes(permission); | |
| console.log(`Is maintainer: ${isMaintainer}`); | |
| core.setOutput('is-maintainer', isMaintainer.toString()); | |
| return isMaintainer; | |
| } catch (error) { | |
| console.error('Error checking permissions:', error); | |
| // On error, default to false (non-maintainer) | |
| core.setOutput('is-maintainer', 'false'); | |
| return false; | |
| } | |
| test-cli-run: | |
| runs-on: ubuntu-latest | |
| needs: check-permissions | |
| # Only run for maintainers who have access to API keys | |
| if: needs.check-permissions.outputs.is-maintainer == 'true' | |
| timeout-minutes: 5 | |
| env: | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.11' | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Create virtual environment | |
| run: uv venv | |
| - name: Install dependencies | |
| run: | | |
| source .venv/bin/activate | |
| uv sync --all-extras | |
| - name: Test shotgun run command | |
| env: | |
| SHOTGUN_LOG_LEVEL: DEBUG | |
| run: | | |
| source .venv/bin/activate | |
| shotgun config set openai --api-key $OPENAI_API_KEY | |
| shotgun run -n "What is 2 + 2? Reply with just the number." | |
| secret-scanning: | |
| name: Scan for secrets | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 # Full history for comprehensive scan | |
| - name: Pull trufflehog Docker image | |
| run: docker pull ghcr.io/trufflesecurity/trufflehog:latest | |
| - name: Run trufflehog scan (pass 1 - all files except uv.lock) | |
| run: docker run --rm -v "$PWD:/repo" ghcr.io/trufflesecurity/trufflehog:latest git file:///repo --fail --no-update --exclude-globs=uv.lock | |
| - name: Run trufflehog scan (pass 2 - uv.lock only, exclude SentryToken) | |
| run: docker run --rm -v "$PWD:/repo" ghcr.io/trufflesecurity/trufflehog:latest git file:///repo --fail --no-update --include-paths=/repo/.github/trufflehog-include-lockfile.txt --exclude-detectors=SentryToken | |
| docker-build-test: | |
| name: Docker build test | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Build Docker image | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| push: false | |
| tags: shotgun:pr-${{ github.event.pull_request.number }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| notify-slack-on-secret-found: | |
| runs-on: ubuntu-latest | |
| needs: [secret-scanning] | |
| if: failure() | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 2 | |
| - name: Get PR and commit information | |
| id: pr_info | |
| run: | | |
| # Get PR information | |
| PR_NUMBER="${{ github.event.pull_request.number }}" | |
| PR_TITLE="${{ github.event.pull_request.title }}" | |
| PR_AUTHOR="${{ github.event.pull_request.user.login }}" | |
| PR_URL="${{ github.event.pull_request.html_url }}" | |
| # Get commit information | |
| COMMIT_SHA=$(git rev-parse HEAD) | |
| COMMIT_SHORT=$(git rev-parse --short HEAD) | |
| COMMIT_MESSAGE=$(git log -1 --pretty=format:"%s") | |
| # Store outputs | |
| echo "pr_number=${PR_NUMBER}" >> $GITHUB_OUTPUT | |
| echo "pr_title=${PR_TITLE}" >> $GITHUB_OUTPUT | |
| echo "pr_author=${PR_AUTHOR}" >> $GITHUB_OUTPUT | |
| echo "pr_url=${PR_URL}" >> $GITHUB_OUTPUT | |
| echo "commit_sha=${COMMIT_SHA}" >> $GITHUB_OUTPUT | |
| echo "commit_short=${COMMIT_SHORT}" >> $GITHUB_OUTPUT | |
| echo "commit_message=${COMMIT_MESSAGE}" >> $GITHUB_OUTPUT | |
| - name: Send Slack notification | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.SHOTGUN_ALPHA_WEBHOOK }} | |
| run: | | |
| # Prepare variables | |
| PR_NUMBER="${{ steps.pr_info.outputs.pr_number }}" | |
| PR_TITLE="${{ steps.pr_info.outputs.pr_title }}" | |
| PR_AUTHOR="${{ steps.pr_info.outputs.pr_author }}" | |
| PR_URL="${{ steps.pr_info.outputs.pr_url }}" | |
| COMMIT_SHORT="${{ steps.pr_info.outputs.commit_short }}" | |
| COMMIT_MESSAGE="${{ steps.pr_info.outputs.commit_message }}" | |
| WORKFLOW_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| REPO_URL="${{ github.server_url }}/${{ github.repository }}" | |
| COMMIT_URL="${REPO_URL}/commit/${{ steps.pr_info.outputs.commit_sha }}" | |
| # Create Slack payload | |
| cat <<EOF > /tmp/slack_payload.json | |
| { | |
| "blocks": [ | |
| { | |
| "type": "header", | |
| "text": { | |
| "type": "plain_text", | |
| "text": "🚨 SECRET DETECTED IN PR", | |
| "emoji": true | |
| } | |
| }, | |
| { | |
| "type": "section", | |
| "text": { | |
| "type": "mrkdwn", | |
| "text": "*Trufflehog detected secrets or API keys in a pull request!*" | |
| } | |
| }, | |
| { | |
| "type": "divider" | |
| }, | |
| { | |
| "type": "section", | |
| "fields": [ | |
| { | |
| "type": "mrkdwn", | |
| "text": "*PR:*\n<${PR_URL}|#${PR_NUMBER}: ${PR_TITLE}>" | |
| }, | |
| { | |
| "type": "mrkdwn", | |
| "text": "*Author:*\n@${PR_AUTHOR}" | |
| }, | |
| { | |
| "type": "mrkdwn", | |
| "text": "*Commit:*\n<${COMMIT_URL}|${COMMIT_SHORT}>" | |
| }, | |
| { | |
| "type": "mrkdwn", | |
| "text": "*Workflow:*\n<${WORKFLOW_URL}|View Details>" | |
| } | |
| ] | |
| }, | |
| { | |
| "type": "divider" | |
| }, | |
| { | |
| "type": "section", | |
| "text": { | |
| "type": "mrkdwn", | |
| "text": "*Action Required:*\n• Review the workflow logs to identify the detected secret\n• Rotate any exposed credentials immediately\n• Remove secrets from the commit history\n• Use environment variables or secret management tools" | |
| } | |
| }, | |
| { | |
| "type": "context", | |
| "elements": [ | |
| { | |
| "type": "mrkdwn", | |
| "text": "🔍 Detected by trufflehog secret scanner" | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| EOF | |
| # Send to Slack | |
| curl -X POST -H 'Content-type: application/json' \ | |
| --data @/tmp/slack_payload.json \ | |
| "$SLACK_WEBHOOK_URL" | |
| all-checks-passed: | |
| runs-on: ubuntu-latest | |
| needs: [validate-pr-title, lint-and-typecheck, test-group-1, test-group-2, test-group-3, coverage-report, test-cli-basics, check-permissions, test-cli-run, secret-scanning, docker-build-test] | |
| if: always() | |
| timeout-minutes: 1 | |
| steps: | |
| - name: Check job results | |
| run: | | |
| echo "validate-pr-title: ${{ needs.validate-pr-title.result }}" | |
| echo "lint-and-typecheck: ${{ needs.lint-and-typecheck.result }}" | |
| echo "test-group-1: ${{ needs.test-group-1.result }}" | |
| echo "test-group-2: ${{ needs.test-group-2.result }}" | |
| echo "test-group-3: ${{ needs.test-group-3.result }}" | |
| echo "coverage-report: ${{ needs.coverage-report.result }}" | |
| echo "test-cli-basics: ${{ needs.test-cli-basics.result }}" | |
| echo "check-permissions: ${{ needs.check-permissions.result }}" | |
| echo "test-cli-run: ${{ needs.test-cli-run.result }}" | |
| echo "secret-scanning: ${{ needs.secret-scanning.result }}" | |
| echo "docker-build-test: ${{ needs.docker-build-test.result }}" | |
| # Required jobs that must always succeed | |
| if [[ "${{ needs.validate-pr-title.result }}" != "success" || \ | |
| "${{ needs.lint-and-typecheck.result }}" != "success" || \ | |
| "${{ needs.test-group-1.result }}" != "success" || \ | |
| "${{ needs.test-group-2.result }}" != "success" || \ | |
| "${{ needs.test-group-3.result }}" != "success" || \ | |
| "${{ needs.coverage-report.result }}" != "success" || \ | |
| "${{ needs.test-cli-basics.result }}" != "success" || \ | |
| "${{ needs.check-permissions.result }}" != "success" || \ | |
| "${{ needs.secret-scanning.result }}" != "success" || \ | |
| "${{ needs.docker-build-test.result }}" != "success" ]]; then | |
| echo "❌ One or more required checks failed or were cancelled" | |
| exit 1 | |
| fi | |
| # test-cli-run can be skipped for non-maintainers, but must succeed for maintainers | |
| if [[ "${{ needs.test-cli-run.result }}" == "failure" || \ | |
| "${{ needs.test-cli-run.result }}" == "cancelled" ]]; then | |
| echo "❌ CLI run test failed or was cancelled" | |
| exit 1 | |
| fi | |
| # If we get here, either all required checks passed, or test-cli-run was skipped | |
| if [[ "${{ needs.test-cli-run.result }}" == "skipped" ]]; then | |
| echo "⏭️ CLI run test skipped (non-maintainer PR)" | |
| fi | |
| echo "✅ All required checks have passed successfully!" |