fix: handle three StockTrim API quirks surfaced via MCP audit (#202) #145
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: Release | |
| on: | |
| push: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| permissions: {} | |
| jobs: | |
| test: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Install UV | |
| uses: astral-sh/setup-uv@v4 | |
| # Temporarily disable cache due to service issues | |
| # with: | |
| # enable-cache: true | |
| - name: Set up Python | |
| run: uv python install 3.13 | |
| - name: Install dependencies | |
| run: uv sync --all-extras --all-packages | |
| - name: Run full CI pipeline | |
| run: uv run poe ci | |
| release-client: | |
| needs: test | |
| runs-on: ubuntu-latest | |
| concurrency: | |
| group: release-client | |
| cancel-in-progress: false | |
| permissions: | |
| id-token: write | |
| contents: write | |
| outputs: | |
| released: ${{ steps.release.outputs.released }} | |
| version: ${{ steps.release.outputs.version }} | |
| tag: ${{ steps.release.outputs.tag }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.SEMANTIC_RELEASE_TOKEN }} | |
| - name: Install UV | |
| uses: astral-sh/setup-uv@v4 | |
| # Temporarily disable cache due to service issues | |
| # with: | |
| # enable-cache: true | |
| - name: Set up Python | |
| run: uv python install 3.13 | |
| - name: Install dependencies | |
| run: uv sync --all-extras | |
| - name: Check for client changes | |
| id: check | |
| run: | | |
| # Check if there are any commits with (client) scope or no scope since last client release | |
| # Pattern explicitly matches: feat/fix/perf with no scope OR (client) scope | |
| # Excludes (mcp) and other scopes to prevent incorrect triggers | |
| if git log $(git describe --tags --abbrev=0 --match="client-v*" 2>/dev/null || echo "HEAD~10")..HEAD --pretty=format:"%s" | grep -qE '^(feat|fix|perf)(:|\\(client\\):)'; then | |
| echo "has_changes=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "has_changes=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Python Semantic Release (Client) | |
| if: steps.check.outputs.has_changes == 'true' | |
| id: release | |
| uses: python-semantic-release/python-semantic-release@v9.15.2 | |
| with: | |
| github_token: ${{ secrets.SEMANTIC_RELEASE_TOKEN }} | |
| root_options: -vv | |
| - name: Build client package | |
| if: steps.release.outputs.released == 'true' | |
| run: | | |
| uv build | |
| mkdir -p client-dist | |
| cp dist/*.whl dist/*.tar.gz client-dist/ | |
| - name: Upload client artifacts | |
| if: steps.release.outputs.released == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: dist-client | |
| path: client-dist/ | |
| release-mcp: | |
| needs: [test, release-client] | |
| # Run if: (1) there are MCP changes OR (2) client was released (MCP needs new client version) | |
| if: always() && needs.test.result == 'success' | |
| runs-on: ubuntu-latest | |
| concurrency: | |
| group: release-mcp | |
| cancel-in-progress: false | |
| permissions: | |
| id-token: write | |
| contents: write | |
| outputs: | |
| released: ${{ steps.release.outputs.released }} | |
| version: ${{ steps.release.outputs.version }} | |
| tag: ${{ steps.release.outputs.tag }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.SEMANTIC_RELEASE_TOKEN }} | |
| - name: Install UV | |
| uses: astral-sh/setup-uv@v4 | |
| # Temporarily disable cache due to service issues | |
| # with: | |
| # enable-cache: true | |
| - name: Set up Python | |
| run: uv python install 3.13 | |
| - name: Install dependencies | |
| run: uv sync --all-extras | |
| - name: Check for MCP changes or client release | |
| id: check | |
| run: | | |
| # Check if there are any commits with (mcp) scope since last mcp release | |
| # This workflow-level check determines if semantic-release should even run | |
| # NOTE: This regex is intentionally strict—only MCP-scoped commits (e.g., feat(mcp):, fix(mcp):, perf(mcp):) | |
| # will trigger an MCP release. This is consistent with the documented release strategy: | |
| # MCP releases are triggered by MCP-only changes, or when the client is released (checked separately below). | |
| if git log $(git describe --tags --abbrev=0 --match="mcp-v*" 2>/dev/null || echo "HEAD~10")..HEAD --pretty=format:"%s" | grep -qE '^(feat|fix|perf)\(mcp\):'; then | |
| has_mcp_changes=true | |
| else | |
| has_mcp_changes=false | |
| fi | |
| echo "has_mcp_changes=${has_mcp_changes}" >> $GITHUB_OUTPUT | |
| # Check if client was released (MCP needs to pick up new client version) | |
| if [ "${{ needs.release-client.outputs.released }}" == "true" ]; then | |
| echo "client_released=true" >> $GITHUB_OUTPUT | |
| echo "should_release=true" >> $GITHUB_OUTPUT | |
| elif [ "${has_mcp_changes}" == "true" ]; then | |
| echo "client_released=false" >> $GITHUB_OUTPUT | |
| echo "should_release=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "client_released=false" >> $GITHUB_OUTPUT | |
| echo "should_release=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Update MCP client dependency | |
| if: steps.check.outputs.client_released == 'true' | |
| run: | | |
| # Install tomli-w for TOML writing | |
| uv pip install tomli-w | |
| # Update stocktrim-openapi-client dependency to use new version | |
| cat > update_client_dep.py << 'EOF' | |
| import tomllib | |
| import tomli_w | |
| import sys | |
| client_version = sys.argv[1] | |
| with open('stocktrim_mcp_server/pyproject.toml', 'rb') as f: | |
| data = tomllib.load(f) | |
| # Update client dependency to specific version | |
| for i, dep in enumerate(data['project']['dependencies']): | |
| if dep.startswith('stocktrim-openapi-client'): | |
| data['project']['dependencies'][i] = f'stocktrim-openapi-client=={client_version}' | |
| break | |
| # Remove workspace source override for PyPI release | |
| if 'tool' in data and 'uv' in data['tool'] and 'sources' in data['tool']['uv']: | |
| if 'stocktrim-openapi-client' in data['tool']['uv']['sources']: | |
| del data['tool']['uv']['sources']['stocktrim-openapi-client'] | |
| with open('stocktrim_mcp_server/pyproject.toml', 'wb') as f: | |
| tomli_w.dump(data, f) | |
| print(f'Updated MCP client dependency to {client_version}') | |
| EOF | |
| uv run python update_client_dep.py "${{ needs.release-client.outputs.version }}" | |
| # Commit the dependency update | |
| # Note: This chore(mcp) commit won't itself trigger a version bump (default_bump_level=0 for chore commits) | |
| # The subsequent semantic-release step will pick up this change and include it in the release | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add stocktrim_mcp_server/pyproject.toml | |
| git diff --cached --quiet || git commit -m "chore(mcp): update client dependency to v${{ needs.release-client.outputs.version }}" | |
| git push | |
| - name: Python Semantic Release (MCP) | |
| if: steps.check.outputs.should_release == 'true' | |
| id: release | |
| uses: python-semantic-release/python-semantic-release@v9.15.2 | |
| with: | |
| github_token: ${{ secrets.SEMANTIC_RELEASE_TOKEN }} | |
| root_options: -vv | |
| directory: stocktrim_mcp_server | |
| - name: Build MCP server package | |
| if: steps.release.outputs.released == 'true' | |
| run: | | |
| rm -rf dist/ | |
| uv build --package stocktrim-mcp-server | |
| mkdir -p mcp-dist | |
| cp dist/*.whl dist/*.tar.gz mcp-dist/ | |
| - name: Upload MCP server artifacts | |
| if: steps.release.outputs.released == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: dist-mcp | |
| path: mcp-dist/ | |
| build-mcpb: | |
| name: Build MCPB bundle | |
| needs: release-mcp | |
| # Only build the .mcpb when the MCP server actually released — the bundle | |
| # is an asset on the mcp-v* GitHub release created by python-semantic-release. | |
| if: needs.release-mcp.outputs.released == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| # Pull the tag commit so the bundle picks up the version bump that | |
| # release-mcp just pushed. | |
| ref: ${{ needs.release-mcp.outputs.tag }} | |
| # ``scripts/build_mcpb.py`` is stdlib-only and shells out to the ``mcpb`` | |
| # CLI — no project deps are needed for this job. Use setup-python | |
| # instead of setup-uv so we don't pull in a project venv resolution we | |
| # don't use. | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.13" | |
| - name: Install Node.js (for mcpb CLI) | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: "20" | |
| - name: Install mcpb CLI | |
| # Pin major version to avoid breaking changes from a future 3.x and to | |
| # narrow the supply-chain surface — the .mcpb artifact this CLI builds | |
| # is attached to a public GitHub release, so a compromised dependency | |
| # would ship to end users. Bump deliberately when a new feature is | |
| # needed; the published manifest_version (0.4) is independent of the | |
| # CLI version. | |
| run: npm install -g @anthropic-ai/mcpb@^2.1.2 | |
| - name: Build .mcpb bundle | |
| id: build | |
| run: | | |
| artifact=$(python scripts/build_mcpb.py) | |
| echo "artifact=${artifact}" >> "$GITHUB_OUTPUT" | |
| ls -la "${artifact}" | |
| - name: Upload .mcpb artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: mcpb | |
| path: ${{ steps.build.outputs.artifact }} | |
| - name: Attach .mcpb to GitHub release | |
| # python-semantic-release created the release matching the mcp-v* tag | |
| # in the release-mcp job. Upload the .mcpb as an additional asset on | |
| # that release. | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh release upload "${{ needs.release-mcp.outputs.tag }}" \ | |
| "${{ steps.build.outputs.artifact }}" --clobber | |
| publish-client: | |
| name: Publish Client to PyPI | |
| needs: release-client | |
| if: needs.release-client.outputs.released == 'true' | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: pypi-client | |
| url: https://pypi.org/p/stocktrim-openapi-client | |
| permissions: | |
| id-token: write | |
| steps: | |
| - name: Download client artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: dist-client | |
| path: dist/ | |
| - name: Publish client to PyPI | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| attestations: true | |
| publish-mcp: | |
| name: Publish MCP Server to PyPI | |
| needs: release-mcp | |
| if: needs.release-mcp.outputs.released == 'true' | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: pypi-mcp | |
| url: https://pypi.org/p/stocktrim-mcp-server | |
| permissions: | |
| id-token: write | |
| steps: | |
| - name: Download MCP server artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: dist-mcp | |
| path: dist/ | |
| - name: Publish MCP server to PyPI | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| attestations: true |