diff --git a/.github/actions/setup-mise-env/action.yml b/.github/actions/setup-mise-env/action.yml index 386b88f9b..d913a282d 100644 --- a/.github/actions/setup-mise-env/action.yml +++ b/.github/actions/setup-mise-env/action.yml @@ -27,6 +27,28 @@ inputs: description: Task profile to use for mise setup one of 'minimal', 'reviewer', 'dev' required: true default: 'minimal' + mise-debug: + description: Enable debug output for mise commands + required: false + default: 'false' + +outputs: + # MISE_PATH, MISE_ENV, and PYTHON_PATH are typically in uppercase and may be case sensitive in some environments + MISE_PATH: + description: Path to mise executable + value: ${{ steps.mise-outputs.outputs.MISE_PATH }} + MISE_ENV: + description: Mise environment name + value: ${{ steps.mise-outputs.outputs.MISE_ENV }} + PROFILE: + description: Profile used for setup + value: ${{ steps.mise-outputs.outputs.PROFILE }} + PYTHON_PATH: + description: Path to Python executable + value: ${{ steps.mise-outputs.outputs.PYTHON_PATH }} + MISE_PYTHON_VERSION: + description: Python version used by mise + value: ${{ steps.mise-outputs.outputs.MISE_PYTHON_VERSION }} runs: using: composite @@ -36,13 +58,39 @@ runs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 with: fetch-depth: ${{ inputs.fetch-depth }} + - name: Free up disk space + shell: bash + run: ./scripts/dev-env/ci-free-disk-space.sh - name: Install system dependencies shell: bash run: | - sudo apt-get update && sudo apt-get install -y zsh curl git - - name: Free up disk space + # ensure build-python dependencies are installed + sudo mkdir -p /etc/apt/apt.conf.d/ && echo 'DPkg::Post-Invoke {"/bin/rm -f /var/cache/apt/archives/*.deb || true";};' | sudo tee /etc/apt/apt.conf.d/99-local-cleanup + sudo apt-get update && sudo apt-get install -y curl git libreadline-dev libbz2-dev zlib1g-dev + + - name: Set up Environment for Free-Threaded Python shell: bash - run: ./scripts/dev-env/ci-free-disk-space.sh + env: + MISE_PYTHON_VERSION: ${{ inputs.python-version }} + UV_PYTHON: ${{ inputs.python-version }} + PYTHON_VERSION: ${{ inputs.python-version }} + MISE_VERBOSE: ${{ inputs.mise-debug }} + run: | + if [[ "${PYTHON_VERSION}" != *t ]]; then + echo "Using standard Python version ${PYTHON_VERSION}..." + echo "MISE_PYTHON_VERSION=${PYTHON_VERSION}" >> "$GITHUB_ENV" + echo "UV_PYTHON=${PYTHON_VERSION}" >> "$GITHUB_ENV" + exit 0 + fi + echo "Setting up environment for free-threaded Python ${PYTHON_VERSION}..." + python_version="${PYTHON_VERSION}" + base_version="${python_version%t}" + echo "Base version: ${base_version}" + # I couldn't get Mise's handling of free-threaded Python versions to work correctly... + # So, instead, we'll let mise install the base version, and use uv to get the free-threaded version + # and then sync uv and mise and tell mise to use that version + echo "MISE_PYTHON_VERSION=${base_version}" >> "$GITHUB_ENV" + echo "UV_PYTHON=${base_version}+freethreaded" >> "$GITHUB_ENV" - name: Setup Mise for Minimal Profile if: inputs.profile == 'minimal' @@ -50,13 +98,16 @@ runs: env: MISE_YES: 1 MISE_EXPERIMENTAL: 1 - UV_PYTHON: ${{ inputs.python-version }} + UV_PYTHON: ${{ env.UV_PYTHON }} + MISE_VERBOSE: ${{ inputs.mise-debug }} + MISE_PYTHON_COMPILE: 0 with: github_token: ${{ inputs.github-token }} experimental: true install: true cache: true cache_key_prefix: ${{ inputs.profile }} + env: false id: mise-setup-minimal - name: Setup Mise for Reviewer Profile if: inputs.profile == 'reviewer' @@ -65,16 +116,22 @@ runs: MISE_YES: 1 MISE_EXPERIMENTAL: 1 MISE_ENV: dev - UV_PYTHON: ${{ inputs.python-version }} + UV_PYTHON: ${{ env.UV_PYTHON }} + MISE_VERBOSE: ${{ inputs.mise-debug }} + MISE_PYTHON_COMPILE: 0 with: github_token: ${{ inputs.github-token }} experimental: true install: true cache: true cache_key_prefix: ${{ inputs.profile }} + env: false - name: Activate Reviewer Profile Environment if: inputs.profile == 'reviewer' shell: bash + env: + MISE_GITHUB_TOKEN: ${{ inputs.github-token }} + MISE_VERBOSE: ${{ inputs.mise-debug }} id: mise-setup-reviewer run: | uv sync @@ -85,23 +142,72 @@ runs: MISE_YES: 1 MISE_EXPERIMENTAL: 1 MISE_ENV: dev - UV_PYTHON: ${{ inputs.python-version }} + MISE_VERBOSE: ${{ inputs.mise-debug }} + UV_PYTHON: ${{ env.UV_PYTHON }} + MISE_PYTHON_COMPILE: 0 with: github_token: ${{ inputs.github-token }} experimental: true install: true cache: true cache_key_prefix: ${{ inputs.profile }} + env: false id: mise-setup-dev + - name: Insert Free-Threaded Python into Dev Profile + if: inputs.profile == 'dev' && endsWith(inputs.python-version, 't') + shell: bash + id: mise-setup-dev-insert + env: + MISE_PYTHON_VERSION: ${{ env.MISE_PYTHON_VERSION }} + UV_PYTHON: ${{ env.UV_PYTHON }} + MISE_VERBOSE: ${{ inputs.mise-debug }} + run: | + echo "Inserting free-threaded Python version $UV_PYTHON into dev profile..." + mise use -g uv@latest + uv python install -U "cpython-${UV_PYTHON}" 2>&1 + # after a lot of trial and error, it seems that combined with setting MISE_PYTHON_VERSION to the base version, and using uv to install the free-threaded version, and syncing uv and mise, this works + # the favored version is now the free-threaded version -- as mise list python will show: + mise sync python --uv + mise list python 2>&1 + - name: Activate Dev Profile Environment + if: inputs.profile == 'dev' + shell: bash + id: mise-setup-dev-activate + env: + MISE_ENV: dev + MISE_EXPERIMENTAL: 1 + MISE_YES: 1 + MISE_GITHUB_TOKEN: ${{ inputs.github-token }} + MISE_PYTHON_VERSION: ${{ env.MISE_PYTHON_VERSION }} + MISE_VERBOSE: ${{ inputs.mise-debug }} + run: | + echo "Activating shims" + eval "$(mise activate bash --shims)" 2>&1 || true + echo "starting cloud setup for $MISE_PYTHON_VERSION" + echo "Mise doctor output:" + mise doctor 2>&1 + echo "Ensuring uv is set to latest..." + mise use -g uv@latest + echo "Starting cloud-setup task..." + mise //:cloud-setup 2>&1 - name: Set outputs shell: bash id: mise-outputs + env: + MISE_PYTHON_VERSION: ${{ env.MISE_PYTHON_VERSION }} + MISE_ENV: ${{ env.MISE_ENV }} + MISE_VERBOSE: ${{ inputs.mise-debug }} + PROFILE: ${{ inputs.profile }} run: | - echo "MISE_PATH=$(which mise)" >> $GITHUB_OUTPUT - echo "MISE_ENV=$MISE_ENV" >> $GITHUB_OUTPUT - echo "PROFILE=${{ inputs.profile }}" >> $GITHUB_OUTPUT - echo "PYTHON_VERSION=$(python --version | awk '{print $2}')" >> $GITHUB_OUTPUT - echo "PYTHON_PATH=$(which python)" >> $GITHUB_OUTPUT - - + MISE_PATH="$(which mise)" + echo "MISE_PATH=$MISE_PATH" >> "$GITHUB_OUTPUT" + mise_bin_dir="$(dirname "$MISE_PATH")" + python_path="$(which python)" + python_bin_dir="$(dirname "$python_path")" + echo "MISE_BIN_DIR=$mise_bin_dir" >> "$GITHUB_OUTPUT" + echo "PYTHON_BIN_DIR=$python_bin_dir" >> "$GITHUB_OUTPUT" + echo "PROFILE=$PROFILE" >> "$GITHUB_OUTPUT" + echo "PYTHON_PATH=$python_path" >> "$GITHUB_OUTPUT" + echo "MISE_PYTHON_VERSION=$MISE_PYTHON_VERSION" >> "$GITHUB_OUTPUT" + echo "MISE_ENV=${MISE_ENV:-}" >> "$GITHUB_OUTPUT" diff --git a/.github/actions/setup-python-env/action.yml b/.github/actions/setup-python-env/action.yml index c90a79aad..8ad9b8db6 100644 --- a/.github/actions/setup-python-env/action.yml +++ b/.github/actions/setup-python-env/action.yml @@ -24,6 +24,14 @@ inputs: required: false default: '' +outputs: + PYTHON_PATH: + description: Path to the Python executable + value: ${{ steps.python-setup.outputs.PYTHON_PATH }} + uv_path: + description: Path to uv executable + value: ${{ steps.python-setup.outputs.uv_path }} + runs: using: composite steps: @@ -45,7 +53,10 @@ runs: allow-prereleases: true - name: Install pip packages shell: bash + id: python-setup run: | python -m pip install --upgrade pip setuptools wheel python -m pip install uv + echo "PYTHON_PATH=$(which python)" >> "$GITHUB_OUTPUT" + echo "uv_path=$(which uv)" >> "$GITHUB_OUTPUT" diff --git a/.github/actions/setup-uv-env/action.yml b/.github/actions/setup-uv-env/action.yml index 918113526..077e44b26 100644 --- a/.github/actions/setup-uv-env/action.yml +++ b/.github/actions/setup-uv-env/action.yml @@ -28,6 +28,14 @@ inputs: required: false default: 'false' +outputs: + uv-path: + description: Path to uv executable + value: ${{ steps.setup-uv.outputs.uv-path }} + uvx-path: + description: Path to uvx executable + value: ${{ steps.setup-uv.outputs.uvx-path }} + runs: using: composite steps: @@ -54,10 +62,4 @@ runs: prune-cache: "true" add-problem-matchers: "true" id: setup-uv - - name: Export outputs - shell: bash - id: uv-outputs - run: | - echo "UV_PATH=${{ steps.setup-uv.outputs.uv-path }}" >> $GITHUB_OUTPUT - echo "UVX_PATH=${{ steps.setup-uv.outputs.uvx-path }}" >> $GITHUB_OUTPUT diff --git a/.github/workflows/_reusable-build.yml b/.github/workflows/_reusable-build.yml index a3f0346f8..b59641359 100644 --- a/.github/workflows/_reusable-build.yml +++ b/.github/workflows/_reusable-build.yml @@ -33,13 +33,15 @@ jobs: build: name: Build Package runs-on: ubuntu-latest + env: + UV_PYTHON: ${{ inputs.python-version }} permissions: contents: read steps: - name: Checkout code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 with: - fetch-depth: 0 + fetch-depth: 2 - name: Setup UV and build uses: ./.github/actions/setup-uv-env @@ -54,7 +56,6 @@ jobs: - name: Build package with UV env: UV_PATH: ${{ steps.setup-uv.outputs.uv-path }} - UV_PYTHON: ${{ inputs.python-version }} run: | if [ "${{ inputs.clean-build }}" = "true" ]; then "$UV_PATH" build --clear diff --git a/.github/workflows/_reusable-lint.yml b/.github/workflows/_reusable-lint.yml index 50e5299a3..e4a061416 100644 --- a/.github/workflows/_reusable-lint.yml +++ b/.github/workflows/_reusable-lint.yml @@ -21,6 +21,11 @@ on: type: boolean default: true +env: + MISE_EXPERIMENTAL: 1 + MISE_YES: 1 + PROFILE: "reviewer" + jobs: lint: permissions: @@ -28,8 +33,8 @@ jobs: name: Lint and Format runs-on: ubuntu-latest env: - MISE_ENV: ci - MISE_EXPERIMENTAL: 1 + MISE_PYTHON_VERSION: ${{ inputs.python-version }} + UV_PYTHON: ${{ inputs.python-version }} steps: - name: Checkout code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 @@ -41,31 +46,19 @@ jobs: with: python-version: ${{ inputs.python-version }} github-token: ${{ secrets.GITHUB_TOKEN }} - profile: "reviewer" + profile: "${{ env.PROFILE }}" skip-checkout: true - id: mise-outputs + id: mise-setup - name: Check code style and linting env: - MISE_YES: 1 - MISE_PYTHON: ${{ inputs.python-version }} - UV_PYTHON: ${{ inputs.python-version }} - MISE_EXPERIMENTAL: 1 - PYTHON_PATH: ${{ steps.mise-outputs.outputs.PYTHON_PATH }} - run: | - export PATH="$HOME/.local/bin:$PATH" - mise //:lint || echo "::warning::Lint checks failed but allowing workflow to continue" + MISE_PYTHON_VERSION: ${{ steps.mise-setup.outputs.MISE_PYTHON_VERSION }} + run: mise run lint || echo "::warning::Lint checks failed but allowing workflow to continue" continue-on-error: true - name: Check formatting and licenses if: inputs.run-format-check env: - MISE_YES: 1 - MISE_PYTHON: ${{ inputs.python-version }} - UV_PYTHON: ${{ inputs.python-version }} - MISE_EXPERIMENTAL: 1 - PYTHON_PATH: ${{ steps.mise-outputs.outputs.PYTHON_PATH }} - run: | - export PATH="$HOME/.local/bin:$PATH" - mise //:format || echo "::warning::License checks or formatting checks failed but allowing workflow to continue" + MISE_PYTHON_VERSION: ${{ steps.mise-setup.outputs.MISE_PYTHON_VERSION }} + run: mise run format || echo "::warning::License checks or formatting checks failed but allowing workflow to continue" continue-on-error: true diff --git a/.github/workflows/_reusable-test.yml b/.github/workflows/_reusable-test.yml index b3e78e042..ea90d0176 100644 --- a/.github/workflows/_reusable-test.yml +++ b/.github/workflows/_reusable-test.yml @@ -39,6 +39,18 @@ on: required: false VOYAGE_API_KEY: required: false +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MISE_EXPERIMENTAL: 1 + MISE_YES: 1 + PROFILE: "dev" + CODEWEAVER_PROJECT_PATH: ${{ github.workspace }} + CODEWEAVER_TESTING: "true" + CODEWEAVER_VECTOR_STORE_URL: ${{ secrets.CODEWEAVER_VECTOR_STORE_URL }} + QDRANT__SERVICE__API_KEY: ${{ secrets.QDRANT__SERVICE__API_KEY }} + VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }} + MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} jobs: test: @@ -57,17 +69,7 @@ jobs: - python-version: "3.14t" experimental: true env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - CODEWEAVER_TESTING: "true" - CODEWEAVER_VECTOR_STORE_URL: ${{ secrets.CODEWEAVER_VECTOR_STORE_URL }} - QDRANT__SERVICE__API_KEY: ${{ secrets.QDRANT__SERVICE__API_KEY }} - VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }} - CODEWEAVER_PROJECT_PATH: ${{ github.workspace }} - MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - MISE_EXPERIMENTAL: 1 - MISE_YES: 1 - MISE_PYTHON: ${{ matrix.python-version }} + MISE_PYTHON_VERSION: ${{ matrix.python-version }} UV_PYTHON: ${{ matrix.python-version }} steps: - name: Checkout code @@ -75,38 +77,29 @@ jobs: with: fetch-depth: 3 - - name: Setup Python environment - if: ${{ matrix.experimental == false }} + - name: Setup Python environment with Mise uses: ./.github/actions/setup-mise-env with: python-version: ${{ matrix.python-version }} github-token: ${{ secrets.GITHUB_TOKEN }} profile: "dev" skip-checkout: true + mise-debug: true id: setup-mise - name: Run quality checks - if: ${{ inputs.run-quality-checks && matrix.experimental == false }} + if: ${{ inputs.run-quality-checks }} env: - MISE_YES: 1 - MISE_PYTHON: ${{ matrix.python-version }} - UV_PYTHON: ${{ matrix.python-version }} - MISE_EXPERIMENTAL: 1 - MISE_PATH: ${{ steps.setup-mise.outputs.MISE_PATH }} + MISE_PYTHON_VERSION: ${{ steps.setup-mise.outputs.MISE_PYTHON_VERSION }} run: | - export PATH="$HOME/.local/bin:$PATH" - "$MISE_PATH" //:check 2>&1 + doctor_output="$(mise doctor)" + echo "$doctor_output" + mise run check 2>&1 continue-on-error: true + - name: Run tests with coverage - if: ${{ matrix.experimental == false }} env: - MISE_YES: 1 - MISE_PYTHON: ${{ matrix.python-version }} - UV_PYTHON: ${{ matrix.python-version }} - MISE_EXPERIMENTAL: 1 - MISE_PATH: ${{ steps.setup-mise.outputs.MISE_PATH }} - run: | - export PATH="$HOME/.local/bin:$PATH" - "$MISE_PATH" //:test-cov -m "${{ inputs.test-markers }}" + MISE_PYTHON_VERSION: ${{ steps.setup-mise.outputs.MISE_PYTHON_VERSION }} + run: mise run test-cov -m "${{ inputs.test-markers }}" - name: Upload coverage to Codecov if: ${{ inputs.upload-coverage && matrix.python-version == '3.12' }} uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 @@ -124,23 +117,3 @@ jobs: test-results.xml coverage.xml if-no-files-found: warn - - # Run experimental last - - name: Setup Experimental Python environment - if: ${{ matrix.experimental == true }} - uses: ./.github/actions/setup-python-env - with: - python-version: ${{ matrix.python-version }} - skip-checkout: true - - name: Setup Env for Experimental Python - if: ${{ matrix.experimental == true }} - shell: bash - run: | - uv sync --no-extra --group test --resolution highest --frozen 2>&1 - - - name: Run test coverage for experimental Python - if: ${{ matrix.experimental == true }} - run: | - export PATH="$HOME/.local/bin:$PATH" - uv run -m pytest tests/ --cov=codeweaver --cov-report=xml --cov-report=term-missing --junit-xml=test-results.xml -v -m "${{ inputs.test-markers }}" - diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04e681975..c2cdf8834 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,6 +51,10 @@ jobs: CODEWEAVER_VECTOR_STORE_URL: ${{ secrets.CODEWEAVER_VECTOR_STORE_URL }} QDRANT__SERVICE__API_KEY: ${{ secrets.QDRANT__SERVICE__API_KEY }} VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }} + with: + test-markers: "not docker and not qdrant and not dev_only and not skip_ci and not network and not external_api and not flaky" + upload-coverage: true + run-quality-checks: true lint: name: Lint @@ -63,4 +67,4 @@ jobs: - test - lint uses: ./.github/workflows/_reusable-build.yml - # No secrets needed for build job + # Build doesn't need secrets. diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index fde6b9af6..05db17a6d 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -19,6 +19,7 @@ on: pull_request_review: types: - submitted + permissions: actions: read checks: read @@ -26,27 +27,26 @@ permissions: contents: write discussions: write pull-requests: write + +env: + CODEWEAVER_VECTOR_STORE_URL: ${{ secrets.CODEWEAVER_VECTOR_STORE_URL }} + QDRANT__SERVICE__API_KEY: ${{ secrets.QDRANT__SERVICE__API_KEY }} + VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }} + CODEWEAVER_PROJECT_PATH: ${{ github.workspace }} + MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MISE_ENV: "dev" + MISE_YES: 1 + TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} + # Limit CodeQL memory usage to prevent OOM kills on standard runners (7GB total RAM) + # Note: Standard runners have ~7GB, so limit to 3GB to leave headroom for system + CODEQL_RAM: 3072 + CODEQL_THREADS: 2 + # Additional limits for CodeQL extractor and evaluator + CODEQL_EXTRACTOR_PYTHON_RAM: 2048 + CODEQL_EVALUATOR_RAM: 2048 jobs: claude-response: - # Alternative: Use ubuntu-latest-4-cores (16GB RAM) if OOM issues persist - # Requires GitHub Team/Enterprise plan runs-on: ubuntu-latest - env: - CODEWEAVER_VECTOR_STORE_URL: ${{ secrets.CODEWEAVER_VECTOR_STORE_URL }} - QDRANT__SERVICE__API_KEY: ${{ secrets.QDRANT__SERVICE__API_KEY }} - VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }} - CODEWEAVER_PROJECT_PATH: ${{ github.workspace }} - MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - MISE_ENV: "dev" - MISE_YES: 1 - TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} - # Limit CodeQL memory usage to prevent OOM kills on standard runners (7GB total RAM) - # Note: Standard runners have ~7GB, so limit to 3GB to leave headroom for system - CODEQL_RAM: 3072 - CODEQL_THREADS: 2 - # Additional limits for CodeQL extractor and evaluator - CODEQL_EXTRACTOR_PYTHON_RAM: 2048 - CODEQL_EVALUATOR_RAM: 2048 steps: - name: Checkout Repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 @@ -61,6 +61,7 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} profile: "reviewer" skip-checkout: "true" + - name: PR Review if: github.event_name == 'pull_request_review' uses: anthropics/claude-code-action@e8bad572273ce919ba15fec95aef0ce974464753 @@ -155,6 +156,7 @@ jobs: - Provide feedback on the code quality, functionality, and adherence to best practices. - Consider the library's existing code style and whether the code aligns with it. - Consider possible security or performance effects. + - If the code does not follow APIs as you would expect, remember that you have access to the context7 tool to look up library documentation. APIs may have changed since your training data. - Suggest improvements or alternatives where applicable. - If the changes are satisfactory and the code passes checks, approve the PR with a comment. - name: Setup Environment for Assigned or Labeled Issues/PRs @@ -185,11 +187,13 @@ jobs: --mcp-config .mcp.json prompt: | REPO: ${{ github.repository }} - ISSUE NUMBER: ${{ github.event.issue.number }} + ISSUE/PR NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }} When you are assigned an issue or it's labeled 'claude': - Your job is to resolve it. - Gather all necessary information about the issue from discussions and comments and the codebase. - If the issue involves external libraries, use the context7 tool to get the latest information on the API. + - Pay attention to external API versions--they may have changed since your training data, or even since the sources you research. When in doubt, use the tavily tool or web search to find the current documentation. + - Research similar issues in this repository and others to inform your approach. - Communicate with the issue reporter for clarification if needed. - Create an issue branch. - Develop a detailed plan to fix the problem. diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 5651e67fb..3e96fff14 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -14,6 +14,7 @@ on: pull_request: paths: - .github/workflows/copilot-setup-steps.yml + jobs: # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. copilot-setup-steps: @@ -22,6 +23,8 @@ jobs: QDRANT__SERVICE__API_KEY: ${{ secrets.QDRANT__SERVICE__API_KEY }} VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }} CODEWEAVER_PROJECT_PATH: ${{ github.workspace }} + MISE_ENV: dev + MISE_EXPERIMENTAL: 1 MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} MISE_YES: 1 # Limit CodeQL memory usage to prevent OOM kills on standard runners (7GB total RAM) @@ -32,23 +35,15 @@ jobs: CODEQL_EXTRACTOR_PYTHON_RAM: 2048 CODEQL_EVALUATOR_RAM: 2048 runs-on: ubuntu-latest - # Alternative: Use ubuntu-latest-4-cores (16GB RAM) if OOM issues persist - # Requires GitHub Team/Enterprise plan - # runs-on: ubuntu-latest-4-cores - # Add an overall job timeout so we don't let runaway installs exhaust the runner indefinitely timeout-minutes: 50 - # Set the permissions to the lowest permissions possible needed for your steps. - # Copilot will be given its own token for its operations. permissions: - # If you want to clone the repository as part of your setup steps, for example to install dependencies, you'll need the `contents: read` permission. contents: write steps: - name: Checkout code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 with: fetch-depth: 2 - - name: Free up disk space - run: ./scripts/dev-env/ci-free-disk-space.sh + - name: Setup Mise Environment uses: ./.github/actions/setup-mise-env with: @@ -56,7 +51,11 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} profile: "dev" skip-checkout: "true" + - name: Make scripts executable run: | chmod -R +x scripts chmod -R +x mise-tasks + + - name: Verify mise setup + run: mise --version diff --git a/.github/workflows/mcp-registry-submit.yml b/.github/workflows/mcp-registry-submit.yml index 9b85ca145..47a68234c 100644 --- a/.github/workflows/mcp-registry-submit.yml +++ b/.github/workflows/mcp-registry-submit.yml @@ -8,7 +8,9 @@ name: Submit to MCP Registry on: release: - types: [published] + types: + - published + - prereleased workflow_dispatch: inputs: version: @@ -29,17 +31,14 @@ jobs: uses: actions/checkout@b3498302c5c423fa896b97a26bb183df735d08f8 with: ref: ${{ github.event.release.tag_name || github.ref }} + fetch-depth: 1 - - name: Set up Python - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c + - name: Setup UV and Python environment + uses: ./.github/actions/setup-uv-env with: python-version: "3.12" - - - name: Install uv - run: | - curl -LsSf "https://astral.sh/uv/install.sh" | sh - echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - + github-token: ${{ secrets.GITHUB_TOKEN }} + id: uv-setup - name: Extract version information id: version run: | @@ -64,91 +63,7 @@ jobs: # Run the generation script (uses PEP 723 dependencies) uv run scripts/build/generate-mcp-server-json.py - - # Update version in generated server.json - python -c " - import json - - with open('server.json', 'r') as f: - data = json.load(f) - - # Update top-level version - data['version'] = '${VERSION}' - - # Update package versions - if 'packages' in data: - for pkg in data['packages']: - pkg['version'] = '${VERSION}' - - with open('server.json', 'w') as f: - json.dump(data, f, indent=2) - - print(f'✓ Regenerated and versioned server.json to ${VERSION}') - " - - - name: Update tools.json with release version - run: | - VERSION="${{ steps.version.outputs.version }}" - # Update the version fields in tools.json - python -c " - import json - import sys - - with open('tools.json', 'r') as f: - tools_data = json.load(f) - - # Update top-level version - tools_data['version'] = '${VERSION}' - # Update server version if present - if 'server' in tools_data: - tools_data['server']['version'] = '${VERSION}' - - with open('tools.json', 'w') as f: - json.dump(tools_data, f, indent=2) - - print(f'Updated tools.json to version ${VERSION}') - " - - - name: Validate server.json format - run: | - python -c " - import json - import sys - - try: - with open('server.json', 'r') as f: - data = json.load(f) - - # Basic validation checks - required_fields = ['name', 'description', 'version'] - missing = [f for f in required_fields if f not in data] - - if missing: - print(f'ERROR: Missing required fields: {missing}') - sys.exit(1) - - # Validate name format (must be namespace/server-name) - if '/' not in data['name']: - print('ERROR: name must be in format namespace/server-name') - sys.exit(1) - - # Validate packages or remotes exist - if 'packages' not in data and 'remotes' not in data: - print('ERROR: Must have either packages or remotes array') - sys.exit(1) - - print('✓ server.json validation passed') - print(f' Name: {data[\"name\"]}') - print(f' Version: {data[\"version\"]}') - print(f' Description: {data[\"description\"][:80]}...') - - except json.JSONDecodeError as e: - print(f'ERROR: Invalid JSON in server.json: {e}') - sys.exit(1) - except Exception as e: - print(f'ERROR: Validation failed: {e}') - sys.exit(1) - " + echo "✓ server.json regenerated" - name: Wait for PyPI package availability run: | @@ -199,103 +114,88 @@ jobs: echo "Please verify that the package was successfully published" exit 1 - - name: Install mcp-publisher CLI - env: - MCP_PUBLISHER_VERSION: "v1.0.0" - run: | - ARCH="$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')" - OS="$(uname -s | tr '[:upper:]' '[:lower:]')" - VERSION="${MCP_PUBLISHER_VERSION:- v1.0.0}" - NO_V_VERSION="${VERSION#v}" - - echo "Downloading mcp-publisher ${VERSION} for ${OS}/${ARCH}..." - curl -L "https://github.com/modelcontextprotocol/registry/releases/download/${VERSION}/mcp-publisher_${NO_V_VERSION}_${OS}_${ARCH}.tar.gz" \ - -o mcp-publisher.tar.gz - curl -L "https://github.com/modelcontextprotocol/registry/releases/download/${VERSION}/mcp-publisher_${NO_V_VERSION}_${OS}_${ARCH}.tar.gz.sha256" \ - -o mcp-publisher.tar.gz.sha256 - - echo "Verifying checksum for mcp-publisher.tar.gz..." - if ! sha256sum -c mcp-publisher.tar.gz.sha256; then - echo "ERROR: Checksum verification failed for mcp-publisher.tar.gz" - exit 1 - fi - - tar xz < mcp-publisher.tar.gz mcp-publisher - sudo mv mcp-publisher /usr/local/bin/ - chmod +x /usr/local/bin/mcp-publisher - rm mcp-publisher.tar.gz mcp-publisher.tar.gz.sha256 - - echo "✓ mcp-publisher installed" - mcp-publisher --version || echo "Version check not available" - - - name: Validate server.json with mcp-publisher - run: | - echo "Validating server.json against MCP schema..." - - # mcp-publisher validate should validate the server.json - # Note: exact command may vary based on mcp-publisher version - if command -v mcp-publisher &> /dev/null; then - mcp-publisher validate server.json || { - echo "ERROR: server.json validation failed" - exit 1 - } - else - echo "WARNING: mcp-publisher validate command not available, skipping" - fi - - name: Submit to MCP Registry env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} MCP_REGISTRY_KEY: ${{ secrets.MCP_REGISTRY_KEY }} + id: submit-mcp run: | echo "Submitting to MCP Registry..." - # Note: The exact submission process depends on mcp-publisher CLI implementation - # and may require manual intervention if the API/CLI has changed. - # The following attempts multiple submission methods in order of preference. - MESSAGE="" - # login to mcp-publisher - if command -v mcp-publisher &> /dev/null; then - echo "Logging in to mcp-publisher..." - mcp-publisher login dns --domain knitli.com --private-key "${MCP_REGISTRY_KEY}" || { - echo "WARNING: mcp-publisher login failed, proceeding without login" - } - MESSAGE="$(mcp-publisher publish)" - else - echo "WARNING: mcp-publisher CLI not available, skipping login" + # we need to login with our domain key first + # the request has three fields: domain (knitli.com), signed_timestamp (hex encoded signature), timestamp (RFC3339) + # we need to sign the timestamp with the private key, which is in MCP_REGISTRY_KEY + # MCP_REGISTRY_KEY is expected to be in PEM format, and it uses ed25519 algorithm + # unfortunately, pkeyutl can only work with files because of how it has to buffer the input for signing + + # setup temporary files and cleanup + umask 077 + indata="$(mktemp)" + key_file="$(mktemp)" + outfile="$(mktemp)" + request_body="$(mktemp)" + # shellcheck disable=SC2329 + cleanup() { + # ensure sensitive files are securely deleted -- on ubuntu, shred should be available + shred -u "$key_file" "$outfile" || rm -f "$key_file" "$outfile" + # these aren't sensitive, but let's keep things tidy: + rm -f "$indata" "$request_body" + } + trap cleanup EXIT + + domain="knitli.com" + timestamp="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + echo -n "$timestamp" > "$indata" + + echo "$MCP_REGISTRY_KEY" > "$key_file" + # it wasn't easy to figure out how to do ed25519 signing with openssl, but this is the correct incantation with recent versions: + openssl pkeyutl -sign -inkey "$key_file" -out "$outfile" -rawin -in "$indata" || { echo "ERROR: Failed to sign timestamp"; exit 1; } + signed_timestamp_hex="$(xxd -p "$outfile" | tr -d '\n')" + { + echo -n '{"domain":"'"$domain"'",' + echo -n '"signed_timestamp":"'"$signed_timestamp_hex"'",' + echo -n '"timestamp":"'"$timestamp"'"}' + } > "$request_body" + echo "Logging in to MCP Registry..." + response="$(curl --request POST \ + --url "https://registry.modelcontextprotocol.io/v0.1/auth/dns" \ + --header "Accept: application/json, application/problem+json" \ + --header "Content-Type: application/json" \ + --data @"$request_body")" + token="$(echo "$response" | jq -r '.registry_token')" + if [ -z "$token" ] || [ "$token" = "null" ]; then + echo "ERROR: Failed to obtain registry token" + echo "Response: $response" + exit 1 fi - if [ -n "$MESSAGE" ] && [[ "$MESSAGE" == *Successfully* ]]; then - echo "✓ Successfully submitted to MCP Registry via mcp-publisher" - echo "$MESSAGE" - exit 0 - else - RESPONSE="$(curl -X POST \ - -H "Authorization: Bearer ${GITHUB_TOKEN}" \ - -H "Content-Type: application/json" \ - -d @server.json \ - -w "\n%{http_code}" \ - https://registry.modelcontextprotocol.io/v0/servers 2>&1)" - - HTTP_CODE="$(echo "$RESPONSE" | tail -n 1)" - BODY="$(echo "$RESPONSE" | head -n -1)" - - if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ]; then - echo "✓ Successfully submitted to MCP Registry (HTTP ${HTTP_CODE})" - exit 0 - else - # Submission failed - provide helpful error message - echo "ERROR: Failed to submit to MCP Registry (HTTP ${HTTP_CODE})" - echo "Response: ${BODY}" - echo "" - echo "This may require manual submission via:" - echo " 1. The registry web interface at https://registry.modelcontextprotocol.io" - echo " 2. The mcp-publisher CLI tool" - echo " 3. Contact registry maintainers for assistance" - exit 1 + echo "✓ Obtained registry token" + publication_response="$(curl --request POST \ + --url https://registry.modelcontextprotocol.io/v0.1/publish \ + --header 'Accept: application/json, application/problem+json' \ + --header "Authorization: Bearer $token" \ + --header 'Content-Type: application/json' \ + --data @./server.json)" + echo "Publication response: $publication_response" + publication_time="$(echo "$publication_response" | jq -r '._meta."io.modelcontextprotocol.registry/official".published_at')" + if [ -z "$publication_time" ] || [ "$publication_time" = "null" ]; then + echo "ERROR: Submission to MCP Registry failed" + # Try to extract error details from the publication response + error_message="$(echo "$publication_response" | jq -r '.error // .message // .detail // empty')" + validation_errors="$(echo "$publication_response" | jq -r '.validation_errors // empty')" + if [ -n "$error_message" ]; then + echo "Error message: $error_message" fi + if [ -n "$validation_errors" ] && [ "$validation_errors" != "null" ]; then + echo "Validation errors: $validation_errors" + fi + echo "Raw response: $publication_response" + exit 1 fi + echo "✓ Successfully submitted to MCP Registry at $publication_time" + echo "MCP_PUBLICATION_TIME=$publication_time" >> "$GITHUB_OUTPUT" + exit 0 - - name: Summary + - name: Summary of MCP Registry Submission if: always() run: | { @@ -303,6 +203,9 @@ jobs: echo "" echo "- **Version**: ${{ steps.version.outputs.version }}" echo "- **Status**: ${{ job.status }}" + if [ -n "${{ steps.submit-mcp.outputs.MCP_PUBLICATION_TIME }}" ]; then + echo "- **Published At**: ${{ steps.submit-mcp.outputs.MCP_PUBLICATION_TIME }}" + fi echo "" } >> "$GITHUB_STEP_SUMMARY" @@ -310,13 +213,15 @@ jobs: { echo "✅ Successfully submitted to MCP Registry" echo "" - echo "View your server at: https://registry.modelcontextprotocol.io/servers/com.knitli/codeweaver" + # Extract and encode server name from server.json + SERVER_NAME=$(jq -r '.name' server.json) + ENCODED_SERVER_NAME=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$SERVER_NAME") + echo "View your server at: https://registry.modelcontextprotocol.io/v0.1/servers/${ENCODED_SERVER_NAME}/versions/${{ steps.version.outputs.version }}" } >> "$GITHUB_STEP_SUMMARY" else { echo "❌ Submission failed" echo "" echo "Please check the workflow logs for details." - echo "You may need to submit manually at: https://registry.modelcontextprotocol.io" } >> "$GITHUB_STEP_SUMMARY" fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6205d2f94..5bdf2c626 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,11 +29,12 @@ jobs: tests: name: Run Tests # EFFICIENCY: Skip tests on tag push (rely on PR tests), but allow manual trigger + # TODO: Add smoke tests for releases if: github.event_name == 'workflow_dispatch' || github.event_name == 'release' uses: ./.github/workflows/_reusable-test.yml with: - python-versions: '["3.12", "3.13"]' - test-markers: "not docker and not qdrant and not dev_only and not skip_ci and not network and not external_api" + python-versions: '["3.12", "3.13", "3.14"]' + test-markers: "not docker and not qdrant and not dev_only and not skip_ci and not network and not external_api and not flaky" upload-coverage: false run-quality-checks: true secrets: inherit diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..b9793da1c --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: 2025 Knitli Inc. +# SPDX-FileContributor: Adam Poulemanos +# +# SPDX-License-Identifier: MIT OR Apache-2.0 + +name: "Stale Issues and PRs" +on: + schedule: + - cron: "0 11 * * *" + +permissions: + issues: write + pull-requests: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d + with: + days-before-stale: 7 + days-before-close: 3 + + any-of-issue-labels: "question,more info" + stale-issue-message: "This issue is stale, we will close it in 3 days if you don't reply." + close-issue-message: "This issue hasn't been active for 10 days, closing." + + any-of-pr-labels: "needs revision" + stale-pr-message: "This PR is stale, we will close it in 3 days if you don't reply." + close-pr-message: "This PR hasn't been active for 10 days, closing." diff --git a/.vscode/terminal.extra.zsh b/.vscode/terminal.extra.zsh index 97524ea97..7260c7eec 100755 --- a/.vscode/terminal.extra.zsh +++ b/.vscode/terminal.extra.zsh @@ -14,23 +14,23 @@ full_setup() { print -P "%F{209}[codeweaver]%f %F{red}Failed to trust the Mise environment!%f" } mise run setup - # Skip 'mise run setup' to avoid circular activation } # Make sure Mise is installed and available print -P "%F{209}[codeweaver]%f Welcome to the%f %F{209}CodeWeaver%f development environment!" print -P "%F{209}[codeweaver]%f If you run into any issues, please visit %F{cyan}https://github.com/knitli/codeweaver/issues%f for assistance." -print -P "%F{209}[codeweaver]%f We're going to set up your environment now..." -REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")" -cd "$REPO_ROOT" || { - print -P "%F{209}[codeweaver]%f %F{red}Failed to change directory to the repository root!%f" -} + +# set a few aliases for convenience +alias mx='mise exec' +alias mr='mise run' + if command -v mise >/dev/null 2>&1; then # We have Mise installed, check if it's initialized for this workspace print -P "%F{209}[codeweaver]%f You're all set!" else + REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo "${PWD}")" # Mise is not installed, run the install script - chmod -R +x "${REPO_ROOT}/scripts" || { + chmod -R +x "${REPO_ROOT}/scripts" || { print -P "%F{209}[codeweaver]%f %F{red}Failed to make install scripts executable!%f" } print -P "%F{209}[codeweaver]%f Installing Mise for the first time..." diff --git a/.vscode/zsh/.zshrc b/.vscode/zsh/.zshrc index 340ba7799..1a2c10edb 100644 --- a/.vscode/zsh/.zshrc +++ b/.vscode/zsh/.zshrc @@ -6,6 +6,27 @@ # VS Code workspace-local zshrc # Loaded because VS Code sets ZDOTDIR to this directory for integrated terminals. +typeset -i _mise_updated + +# replace default mise hook +function _mise_hook { + local diff=${__MISE_DIFF} + source <(command mise hook-env -s zsh) + [[ ${diff} == ${__MISE_DIFF} ]] + _mise_updated=$? +} + +_PROMPT="❱ " # or _PROMPT=${PROMPT} to keep the default + +function _prompt { + if (( ${_mise_updated} )); then + PROMPT='%F{blue}${_PROMPT}%f' + else + PROMPT='%(?.%F{green}${_PROMPT}%f.%F{red}${_PROMPT}%f)' + fi +} + + # Source repo dev shell init (idempotent), then hand off to user's normal zshrc. REPO_ROOT="${PWD}" if [[ -n "${VSCODE_CWD:-}" ]]; then @@ -17,6 +38,17 @@ if [[ -f "${REPO_ROOT}/scripts/dev-env/dev-shell-init.zsh" ]]; then source "${REPO_ROOT}/scripts/dev-env/dev-shell-init.zsh" fi +if [[ -f "${ZDOTDIR:-${HOME}}/.vscode/terminal.extra.zsh" ]]; then + source "${ZDOTDIR:-${HOME}}/.vscode/terminal.extra.zsh" +fi + +# PERSONALIZE: You can add more *local* customizations in .vscode/terminal.local.zsh +# (This file is gitignored so it won't be checked in.) +if [[ -f "${ZDOTDIR:-${HOME}}/.vscode/terminal.local.zsh" ]]; then + source "${ZDOTDIR:-${HOME}}/.vscode/terminal.local.zsh" +fi + + # After workspace init, source the user's real ~/.zshrc if it exists if [[ -f "${HOME}/.zshrc" ]]; then # Avoid infinite loop if ZDOTDIR points here @@ -27,8 +59,8 @@ if [[ -f "${HOME}/.zshrc" ]]; then source "${HOME}/.zshrc" fi -# PERSONALIZE: You can add more *local* customizations in .vscode/terminal.local.zsh -# (This file is gitignored so it won't be checked in.) -if [[ -f "${ZDOTDIR:-${HOME}}/.vscode/terminal.local.zsh" ]]; then - source "${ZDOTDIR:-${HOME}}/.vscode/terminal.local.zsh" -fi \ No newline at end of file +autoload -Uz add-zsh-hook 2>/dev/null +if (( $+functions[add-zsh-hook] )); then + add-zsh-hook precmd _prompt +fi + diff --git a/mise.dev.toml b/mise.dev.toml index c7a2b942d..81a13eaee 100644 --- a/mise.dev.toml +++ b/mise.dev.toml @@ -4,8 +4,6 @@ # SPDX-License-Identifier: MIT OR Apache-2.0 [settings.python] -uv_venv_auto = true -compile = true [tools] hk = "latest" @@ -14,15 +12,17 @@ usage = "latest" uv = "latest" # Quality & validation tools "pipx:reuse" = "latest" # License validation -"pipx:tombi" = "latest" -"pipx:ty" = "latest" # Type checking +"aqua:tombi-toml/tombi" = "latest" +"ubi:astral-sh/ty" = "latest" ruff = "latest" # Python linting/formatting +[env] +_.python.venv = { path = ".venv" } + [hooks] [hooks.enter] env.MISE_EXPERIMENTAL = "1" script = ''' -mise //:enter ''' [tasks] @@ -30,6 +30,11 @@ mise //:enter silent = "stdout" tools.uv = "latest" run = ''' +# profile is set to 'dev' in a cloud CI environment for cloud agents. 'enter' tends to break there. +if [ "$profile" = "dev" ] && [ "$CI" = "true" ]; then + # don't run + exit 0 +fi case "$python_path" in *.venv/bin/python*) # Already in venv, do nothing @@ -43,6 +48,10 @@ esac run_windows = ''' @echo off set "pythonPath=%python_path%" +if "%profile%"=="dev" if "%CI%"=="true" ( + rem don't run + exit /b 0 +) if not "%pythonPath%"=="%pythonPath:.venv\Scripts\python=%" ( rem Already in venv, do nothing ) else ( @@ -58,6 +67,53 @@ ACTIVE_SHELL = '''{{ exec(command='basename "$SHELL" 2>/dev/null || echo zsh') } python_path = '''{{ exec(command='command -v python 2>/dev/null || echo ""') }}''' MISE_EXPERIMENTAL = "1" +[tasks.cloud-setup] +run = [ + # This setup only runs in cloud environments, and for cloud agents or complex tasks. + # For cloud agents, the shell is *always* bash and may or may not be interactive. + "echo \"[codeweaver] Setting up cloud development environment...\"", + "ln -sf AGENTS.md CLAUDE.md", + '''if ! command -v mise &>/dev/null; then + echo "[codeweaver] We couldn't find mise in your PATH. We'll try to install it for you." + if [ -d "$HOME/.local/share/mise" ] || [ -d "$HOME/.mise" ] || [ -f "$HOME/.local/bin/mise" ]; then + export PATH="$HOME/.local/bin:$HOME/.mise/bin:$HOME/.local/share/mise/bin:$PATH" + else + chmod +x scripts/dev-env/install-mise.sh 2>&1 || true + ./scripts/dev-env/install-mise.sh 2>&1 || true + fi + fi''', + "echo \"[codeweaver] Using mise...\"", + '''if command -v mise &>/dev/null && ! grep -q "mise activate" "$HOME/.bashrc"; then + echo "eval \"$(mise activate bash)\" 2>&1 || true" >> "$HOME/.bashrc" + fi''', + '''if command -v mise &>/dev/null && ! grep -q "mise activate" "$HOME/.bash_profile"; then + echo "eval \"$(mise activate bash --shims)\" 2>&1 || true" >> "$HOME/.bash_profile" + fi''', + '''mise trust --all 2>&1''', + '''mise -vv install 2>&1''', + '''mise x hk@latest -- hk install --mise 2>&1 || { echo "[WARNING] hk install failed during cloud-setup" >&2; }''', + '''echo "[codeweaver] Ensuring Python version "$MISE_PYTHON_VERSION" is installed..."''', + "uv venv --allow-existing --seed --keyring-provider subprocess --python $MISE_PYTHON_VERSION .venv 2>&1", + ". .venv/bin/activate 2>&1 || true", + "uv sync --extra full --group dev --group test 2>&1", + "git config include.path .gitconfig 2>&1 || true", + '''bash -c " + ignore_files=('src/codeweaver/_version.py' 'coverage.xml' 'test-results.xml') + for file in \"\${ignore_files[@]}\"; do + if ! grep -qx \"\$file\" .git/info/exclude; then + echo \"\$file\" >> .git/info/exclude 2>&1 + fi + done + "''', + "echo \"[codeweaver] Cloud development environment setup complete!\"", +] + +[tasks.cloud-setup.env] +MISE_PYTHON_VERSION = "{{ get_env(name='MISE_PYTHON_VERSION', default='3.13') }}" +MISE_EXPERIMENTAL = "1" +MISE_ENV = "dev" +MISE_YES = "1" + [tasks.update-dependencies] tools.uv = "latest" depends = ["source"] @@ -107,7 +163,7 @@ uv run pytest tests/ -f --tb=short # Building and packaging run = ''' echo "${CW_PREFIX} Building package..." -uv build && +uv build 2>&1 && echo "${CW_PREFIX} ✅ Package built successfully!" ''' run_windows = ''' diff --git a/mise.toml b/mise.toml index 988a6f217..cef3a6423 100644 --- a/mise.toml +++ b/mise.toml @@ -28,24 +28,26 @@ _.path = [ ] MISE_ENV = '''{% if get_env(name="CI", default="") == "true" %}""{% else %}dev{% endif %}''' CW_PREFIX = '''{% if get_env(name="CI", default="") == "true" %}[codeweaver]{% elif get_env(name="TERM", default="xterm-256color") == "*truecolor*" %}\033[38;2;181;108;48;m[codeweaver]\033[0m{% else %}\033[38;5;131m[codeweaver]\033[0m{% endif %}''' +# this is a bit redundant but it makes it easier to override in CI environments +MISE_PYTHON_VERSION = '''{{ get_env(name="MISE_PYTHON_VERSION", default="3.13") }}''' +HK_MISE = "1" [settings] idiomatic_version_file_enable_tools = [] experimental = true [settings.python] -compile = true -uv_venv_create_args = [ - "--seed", - "--keyring-provider", - "subprocess", - "--python", - "{{ get_env(name='MISE_PYTHON', default='3.13') }}", -] [tools] # most tools are defined in mise.dev.toml to keep CI workflows lightweight -python = "3.13" +# Where needed, tools are defined at the task level here, which allows for more granular control +python = '''{{ get_env(name="MISE_PYTHON_VERSION", default="3.13") }}''' +uv = "latest" + +# Quick note for those unfamiliar with mise: +# - despite the namespace, tools prefixed with "pipx:" are installed via "uv" (I assume once upon a time they were installed with pipx before uv came along) +# - some tools have no prefix -- these are defined in mise's internal registry directly +# - The prefix refers to the "backend" used to install the tool -- most of the no-prefix tools are installed through `aqua` or github/gitlab releases, and failing that, `ubi`. Language-specific package managers (cargo/npm/go) are also used where appropriate. [tasks.info] description = "Print project information" @@ -82,7 +84,11 @@ run = ''' # Source the virtual environment echo "${CW_PREFIX} Sourcing virtual environment..." git rev-parse --show-toplevel || true -source .venv/bin/activate +if [ ! -d ".venv" ]; then + echo "${CW_PREFIX} ⚠️ \033[0;31mVirtual environment not found. We'll try to proceed, but things may break!\033[0m" + uv venv --seed --keyring-provider subprocess --python {{ get_env(name='MISE_PYTHON_VERSION', default='3.13') }} .venv +fi +. .venv/bin/activate || echo "${CW_PREFIX} ⚠️ \033[0;31mFailed to source virtual environment.\033[0m" ''' run_windows = ''' echo [codeweaver] Sourcing virtual environment... @@ -95,12 +101,10 @@ git rev-parse --show-toplevel run = [ "echo \"${CW_PREFIX} Setting up development environment for the first time...\"", "mise set &>/dev/null", - "mise trust &>/dev/null", - "mise install &>/dev/null || true", - "hk install --mise &>/dev/null || true", + "mise trust --all &>/dev/null", + "mise install &>/dev/null || echo \"${CW_PREFIX} ⚠️ WARNING: mise install failed during setup.\"", "mise //:venv &>/dev/null", "mise //:sync &>/dev/null", - "mise env &>/dev/null || true", "git config include.path .gitconfig &>/dev/null || true", "echo 'src/codeweaver/_version.py' >> .git/info/exclude", "echo 'coverage.xml' >> .git/info/exclude", @@ -111,12 +115,10 @@ run = [ run_windows = [ "echo [codeweaver] Setting up development environment for the first time...", "mise set > NUL 2>&1", - "mise trust > NUL 2>&1", - "mise install > NUL 2>&1 || echo.", - "hk install --mise > NUL 2>&1 || echo.", + "mise trust --all > NUL 2>&1", + "mise install > NUL 2>&1 || echo \"[codeweaver] WARNING: mise install failed during setup.\"", "mise //:venv > NUL 2>&1", "mise //:sync > NUL 2>&1", - "mise env > NUL 2>&1", "git config include.path .gitconfig > NUL 2>&1", "echo src/codeweaver/_version.py >> .git\\info\\exclude", "echo coverage.xml >> .git\\info\\exclude", @@ -155,6 +157,9 @@ sources = ["{src,scripts,tests,mise-tasks}/*.py"] [tasks.check] tools.hk = "latest" tools.pkl = "latest" +tools."pipx:ty" = "latest" +tools."pipx:reuse" = "latest" +tools.ruff = "latest" run = ''' echo "${CW_PREFIX} Running quality checks..." hk check @@ -168,7 +173,7 @@ hk check tools.uv = "latest" run = ''' echo "Running linting checks..." -if [[ "$CI" == "true" ]]; then +if [ "$CI" = "true" ]; then uv run ruff check . --output-format=github else uv run ruff check . @@ -194,11 +199,10 @@ hk fix ''' [tasks.format.tools] -"cargo:rust-parallel" = "latest" -"cargo:typos" = "latest" +"aqua:typos" = "latest" "pipx:reuse" = "latest" -"pipx:tombi" = "latest" -"pipx:ty" = "latest" +"aqua:tombi-toml/tombi" = "latest" +"ubi:astral-sh/ty" = "latest" actionlint = "latest" hk = "latest" pkl = "latest" @@ -212,13 +216,10 @@ echo "${CW_PREFIX} Applying formatting...hold onto your butts 🚬...🦖 " # mise -y //:fix-ruff-patterns || { # there are some bugs to work out # echo "${CW_PREFIX} ⚠️ \033[0;32mRuff pattern fixer encountered issues.\033[0m"} uv run ruff format . -files="$(git ls-files "*.yaml" "*.yml" "*.json" 2>/dev/null)" -if [ -n "$files" ]; then - echo "$files" | rust-parallel -d stderr -i - --no-run-if-empty yq -iP --indent=2 "." || true -fi -files="$(git ls-files "*.toml" 2>/dev/null)" -if [ -n "$files" ]; then - tombi format $files 2>/dev/null || true +git ls-files "*.yaml" "*.yml" "*.json" 2>/dev/null | xargs -r yq -iP --indent=2 eval "." 2>/dev/null || true +toml_files="$(git ls-files "*.toml" 2>/dev/null)" +if [ -n "$toml_files" ]; then + printf '%s\n' "$toml_files" | xargs -r tombi format 2>/dev/null || true fi ''' run_windows = ''' @@ -233,9 +234,8 @@ for /f "delims=" %%f in ('git ls-files "*.toml" 2^>NUL') do ( ''' [tasks.format-fix.tools] -"cargo:rust-parallel" = "latest" -"pipx:tombi" = "latest" -"yq" = "latest" +"aqua:tombi-toml/tombi" = "latest" +yq = "latest" uv = "latest" [tasks.graph] @@ -254,15 +254,15 @@ env.MISE_EXPERIMENTAL = "1" tools.uv = "latest" run = ''' echo "${CW_PREFIX} Setting up Python virtual environment..." -uv venv --allow-existing --seed --keyring-provider subprocess --python 3.13 --refresh .venv && +uv venv --allow-existing --seed --keyring-provider subprocess --python {{ get_env(name="MISE_PYTHON_VERSION", default="3.13") }} --refresh .venv && echo "${CW_PREFIX} ✅ Virtual environment is ready!" -source .venv/bin/activate || echo "${CW_PREFIX} ⚠️ \033[0;31mFailed to source virtual environment.\033[0m" +. .venv/bin/activate || echo "${CW_PREFIX} ⚠️ \033[0;31mFailed to source virtual environment.\033[0m" echo "${CW_PREFIX} Running dependency sync..." mise //:sync & ''' run_windows = ''' echo [codeweaver] Setting up Python virtual environment... -uv venv --allow-existing --seed --keyring-provider subprocess --python 3.13 --refresh .venv +uv venv --allow-existing --seed --keyring-provider subprocess --python {{ get_env(name="MISE_PYTHON_VERSION", default="3.13") }} --refresh .venv if %ERRORLEVEL% equ 0 ( echo [codeweaver] Virtual environment is ready! call .venv\Scripts\activate.bat @@ -276,7 +276,7 @@ start /b mise //:sync [tasks.test] # Testing -tools.python = '''{{ get_env(name="MISE_PYTHON", default="3.13") }}''' +tools.python = '''{{ get_env(name="MISE_PYTHON_VERSION", default="3.13") }}''' tools.uv = "latest" usage = ''' args "[dir]" help="directory to test" default="tests/" @@ -293,7 +293,7 @@ uv run pytest "%usage_dir%" %usage_marks:~-3% -v %* [tasks.test-cov] tools.uv = "latest" -tools.python = '''{{ get_env(name="MISE_PYTHON", default="3.13") }}''' +tools.python = '''{{ get_env(name="MISE_PYTHON_VERSION", default="3.13") }}''' run = ''' echo "${CW_PREFIX} Running tests with coverage..." uv sync --group test --inexact diff --git a/pyproject.toml b/pyproject.toml index eb002feff..eb55ff631 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,7 +120,7 @@ dependencies = [ "psutil>=7.1.3", # Core data models and validation "py-cpuinfo>=9.0.0", - "pydantic>=2.12.4", + "pydantic>=2.12.5", "pydantic-ai>=1.27.0", "pydantic-graph>=1.27.0", "pydantic-settings[toml,yaml]>=2.12.0", # Pulls: tomli>=2.0.1, pyyaml>=6.0.1 diff --git a/scripts/dev-env/dev-shell-init.zsh b/scripts/dev-env/dev-shell-init.zsh index ef978827e..f2174345a 100755 --- a/scripts/dev-env/dev-shell-init.zsh +++ b/scripts/dev-env/dev-shell-init.zsh @@ -18,7 +18,7 @@ __cw_this_file="${(%):-%N}" __cw_dirname() { builtin cd -- "${1%/*}" 2>/dev/null && pwd; } __cw_script_dir="$(__cw_dirname "$__cw_this_file")" -# Compute repo root (script lives in /scripts) +# Compute repo root (script lives in /scripts/dev-env/) if [[ -n "$__cw_script_dir" ]]; then REPO_ROOT="$(builtin cd "${__cw_script_dir}/../.." 2>/dev/null && pwd)" fi @@ -44,19 +44,4 @@ if [[ ! -f "${_cw_venv_activate}" && -z "${CODEWEAVER_SILENT_SHELL:-}" ]]; then print -P "%F{yellow}[codeweaver]%f Create it with: %F{green}mise run venv "$REPO_ROOT/.venv" %f" fi -# Source optional workspace extras if provided -_cw_extras="${REPO_ROOT}/.vscode/terminal.extra.zsh" -if [[ -f "${_cw_extras}" ]]; then - # shellcheck disable=SC1090 - source "${_cw_extras}" -fi - -# source optional (per-user) local workspace extras if provided -# this file is gitignored -_cw_local="${REPO_ROOT}/.vscode/terminal.local.zsh" -if [[ -f "${_cw_local}" ]]; then - # shellcheck disable=SC1090 - source "${_cw_local}" -fi - -unset __cw_this_file __cw_script_dir _cw_repo_venv_path _cw_venv_activate _cw_extras +unset __cw_this_file __cw_script_dir _cw_repo_venv_path _cw_venv_activate diff --git a/uv.lock b/uv.lock index c27da6f7a..4e86b8268 100644 --- a/uv.lock +++ b/uv.lock @@ -858,7 +858,7 @@ requires-dist = [ { name = "py-cpuinfo", marker = "extra == 'provider-fastembed'" }, { name = "py-cpuinfo", marker = "extra == 'provider-fastembed-gpu'" }, { name = "py-cpuinfo", marker = "extra == 'provider-sentence-transformers'" }, - { name = "pydantic", specifier = ">=2.12.4" }, + { name = "pydantic", specifier = ">=2.12.5" }, { name = "pydantic-ai", specifier = ">=1.27.0" }, { name = "pydantic-ai-slim", extras = ["anthropic"], marker = "extra == 'provider-anthropic'" }, { name = "pydantic-ai-slim", extras = ["bedrock"], marker = "extra == 'provider-bedrock'" }, @@ -3847,7 +3847,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.4" +version = "2.12.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -3855,9 +3855,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" }, + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] [package.optional-dependencies]