v1.5.8のリリース(品質改善アップデート) #707
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: CI | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| types: [opened, synchronize, reopened, ready_for_review] | |
| workflow_dispatch: | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| FORCE_COLOR: 1 | |
| TOX_TESTENV_PASSENV: FORCE_COLOR | |
| DEFAULT_PYTHON_VERSION: "3.11" | |
| jobs: | |
| # ============================================ | |
| # Should-Run Check - Determines if CI is needed | |
| # main push / workflow_dispatch: 常に実行 | |
| # PR: src/ tests/ pyproject.toml tox.ini の変更時のみ実行 | |
| # ============================================ | |
| should-run: | |
| name: Check if CI needed | |
| runs-on: ubuntu-latest | |
| outputs: | |
| run_ci: ${{ steps.check.outputs.run_ci }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Check changed files | |
| id: check | |
| run: | | |
| # workflow_dispatch: always run | |
| if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then | |
| echo "run_ci=true" >> $GITHUB_OUTPUT | |
| echo "Manual trigger - CI required" | |
| exit 0 | |
| fi | |
| # For main branch push: always run | |
| if [ "${{ github.ref }}" == "refs/heads/main" ]; then | |
| echo "run_ci=true" >> $GITHUB_OUTPUT | |
| echo "Main branch - CI required" | |
| exit 0 | |
| fi | |
| # For PRs: check changed files | |
| BASE="${{ github.event.pull_request.base.sha }}" | |
| HEAD="${{ github.sha }}" | |
| CHANGED=$(git diff --name-only "$BASE" "$HEAD") | |
| echo "Changed files:" | |
| echo "$CHANGED" | |
| if echo "$CHANGED" | grep -qE '^(src|tests)/|^pyproject\.toml$|^tox\.ini$'; then | |
| echo "run_ci=true" >> $GITHUB_OUTPUT | |
| echo "→ src/tests changed: CI required" | |
| else | |
| echo "run_ci=false" >> $GITHUB_OUTPUT | |
| echo "→ no src/tests changes: CI skipped" | |
| fi | |
| # ============================================ | |
| # Lint Job - All branches | |
| # ============================================ | |
| lint: | |
| name: Lint (Ruff) | |
| needs: should-run | |
| if: needs.should-run.outputs.run_ci == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: ${{ env.DEFAULT_PYTHON_VERSION }} | |
| - name: Install tox | |
| run: pip install tox | |
| - name: Run python -m tox -e lint | |
| run: python -m tox -e lint | |
| # ============================================ | |
| # Type Check Job - All branches | |
| # ============================================ | |
| type: | |
| name: Type Check (mypy) | |
| needs: should-run | |
| if: needs.should-run.outputs.run_ci == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: ${{ env.DEFAULT_PYTHON_VERSION }} | |
| - name: Install tox | |
| run: pip install tox | |
| - name: Run python -m tox -e type | |
| run: python -m tox -e type | |
| # ============================================ | |
| # Test Job - Matrix based on branch | |
| # main: Python 3.9-3.14 × all OS | |
| # other: Python 3.13 × each OS (limited) | |
| # ============================================ | |
| test: | |
| name: Test (py${{ matrix.python-version }}, ${{ matrix.os }}) | |
| needs: should-run | |
| if: needs.should-run.outputs.run_ci == 'true' | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 15 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, windows-latest, macos-latest] | |
| python-version: ${{ github.ref == 'refs/heads/main' && fromJSON('["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]') || fromJSON('["3.13"]') }} | |
| exclude: | |
| # macOS: limit to 2 popular versions (3.11, 3.13) | |
| - os: macos-latest | |
| python-version: "3.9" | |
| - os: macos-latest | |
| python-version: "3.10" | |
| - os: macos-latest | |
| python-version: "3.12" | |
| - os: macos-latest | |
| python-version: "3.14" | |
| # Windows: limit to 2 popular versions (3.11, 3.13) | |
| - os: windows-latest | |
| python-version: "3.9" | |
| - os: windows-latest | |
| python-version: "3.10" | |
| - os: windows-latest | |
| python-version: "3.12" | |
| - os: windows-latest | |
| python-version: "3.14" | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| with: | |
| enable-cache: true | |
| - name: Set up Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| # Windows: TEMP optimization | |
| - name: Setup Windows temp directory | |
| if: runner.os == 'Windows' | |
| shell: powershell | |
| run: New-Item -ItemType Directory -Force -Path D:\\temp | |
| - name: Install dependencies | |
| run: | | |
| uv pip install --system pytest pytest-asyncio pytest-xdist pytest-cov apsw pydantic cryptography orjson lru-dict | |
| uv pip install --system -e ".[dev]" | |
| # Windows: TEMP optimization + fixed parallelism | |
| - name: Run tests (Windows) | |
| if: runner.os == 'Windows' | |
| env: | |
| TEMP: 'D:\\temp' | |
| TMP: 'D:\\temp' | |
| run: | | |
| python -m pytest tests/ -v --tb=short --junitxml=test-results.xml --cov=nanasqlite --cov-report=xml:coverage.xml -n 4 | |
| # Linux/macOS: auto parallelism | |
| - name: Run tests (Linux/macOS) | |
| if: runner.os != 'Windows' | |
| run: | | |
| python -m pytest tests/ -v --tb=short --junitxml=test-results.xml --cov=nanasqlite --cov-report=xml:coverage.xml -n auto | |
| - name: Upload coverage to Artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: coverage-report-${{ matrix.os }}-py${{ matrix.python-version }} | |
| path: coverage.xml | |
| retention-days: 7 | |
| - name: Upload test results | |
| uses: actions/upload-artifact@v7 | |
| if: always() | |
| with: | |
| name: test-results-${{ matrix.os }}-py${{ matrix.python-version }} | |
| path: test-results.xml | |
| retention-days: 7 | |
| # ============================================ | |
| # All-Green Summary - Always runs | |
| # skipped は正常扱い(src/tests 未変更の場合) | |
| # ============================================ | |
| summary: | |
| name: ✅ All Checks | |
| needs: [should-run, lint, type, test] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 3 | |
| if: always() | |
| steps: | |
| - name: Check job results | |
| run: | | |
| echo "" | |
| echo "📝 Job statuses:" | |
| echo "📝 should-run → run_ci=${{ needs.should-run.outputs.run_ci }}" | |
| echo "📝 lint → ${{ needs.lint.result == 'success' && '✓ success' || needs.lint.result == 'skipped' && '⏭ skipped' || format('✗ {0}', needs.lint.result) }} [required]" | |
| echo "📝 type → ${{ needs.type.result == 'success' && '✓ success' || needs.type.result == 'skipped' && '⏭ skipped' || format('✗ {0}', needs.type.result) }} [required]" | |
| echo "📝 test → ${{ needs.test.result == 'success' && '✓ success' || needs.test.result == 'skipped' && '⏭ skipped' || format('✗ {0}', needs.test.result) }} [required]" | |
| echo "" | |
| # skipped は正常扱い(src/tests 未変更によるスキップ) | |
| FAILED=false | |
| for result in \ | |
| "${{ needs.lint.result }}" \ | |
| "${{ needs.type.result }}" \ | |
| "${{ needs.test.result }}"; do | |
| if [[ "$result" != "success" && "$result" != "skipped" ]]; then | |
| FAILED=true | |
| fi | |
| done | |
| if [ "$FAILED" = true ]; then | |
| echo "❌ One or more CI checks failed." | |
| exit 1 | |
| fi | |
| if [ "${{ needs.should-run.outputs.run_ci }}" == "false" ]; then | |
| echo "# ⏭️ CI skipped (no src/tests changes) ✓" | |
| else | |
| echo "# ✅ All CI checks passed 🎉🎉🎉" | |
| fi | |
| - name: Generate Summary | |
| if: always() | |
| run: | | |
| echo "## 🔍 CI Pipeline Results" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Stage | Status |" >> $GITHUB_STEP_SUMMARY | |
| echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY | |
| case "${{ needs.lint.result }}" in | |
| success) echo "| 🧹 Lint (ruff) | ✅ Passed |" >> $GITHUB_STEP_SUMMARY ;; | |
| cancelled) echo "| 🧹 Lint (ruff) | ⚠️ Cancelled |" >> $GITHUB_STEP_SUMMARY ;; | |
| skipped) echo "| 🧹 Lint (ruff) | ⏭️ Skipped |" >> $GITHUB_STEP_SUMMARY ;; | |
| *) echo "| 🧹 Lint (ruff) | ❌ Failed |" >> $GITHUB_STEP_SUMMARY ;; | |
| esac | |
| case "${{ needs.type.result }}" in | |
| success) echo "| 🔍 Type (mypy) | ✅ Passed |" >> $GITHUB_STEP_SUMMARY ;; | |
| cancelled) echo "| 🔍 Type (mypy) | ⚠️ Cancelled |" >> $GITHUB_STEP_SUMMARY ;; | |
| skipped) echo "| 🔍 Type (mypy) | ⏭️ Skipped |" >> $GITHUB_STEP_SUMMARY ;; | |
| *) echo "| 🔍 Type (mypy) | ❌ Failed |" >> $GITHUB_STEP_SUMMARY ;; | |
| esac | |
| case "${{ needs.test.result }}" in | |
| success) echo "| 🧪 Tests | ✅ Passed |" >> $GITHUB_STEP_SUMMARY ;; | |
| cancelled) echo "| 🧪 Tests | ⚠️ Cancelled |" >> $GITHUB_STEP_SUMMARY ;; | |
| skipped) echo "| 🧪 Tests | ⏭️ Skipped |" >> $GITHUB_STEP_SUMMARY ;; | |
| *) echo "| 🧪 Tests | ❌ Failed |" >> $GITHUB_STEP_SUMMARY ;; | |
| esac | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Branch:** \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY | |
| if [ "${{ github.ref }}" == "refs/heads/main" ]; then | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "---" >> $GITHUB_STEP_SUMMARY | |
| echo "🚀 **Main branch** - Ready for release if all checks pass" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### 📋 Local Commands" >> $GITHUB_STEP_SUMMARY | |
| echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY | |
| echo "python -m tox -e lint # Ruff linter" >> $GITHUB_STEP_SUMMARY | |
| echo "python -m tox -e type # mypy type check" >> $GITHUB_STEP_SUMMARY | |
| echo "pytest tests/ # Run tests" >> $GITHUB_STEP_SUMMARY | |
| echo "\`\`\`" >> $GITHUB_STEP_SUMMARY | |
| # ============================================ | |
| # Check Version - Main branch only | |
| # ============================================ | |
| check-version: | |
| name: Check Version | |
| needs: [summary] | |
| runs-on: ubuntu-latest | |
| if: github.ref == 'refs/heads/main' && needs.summary.result == 'success' | |
| outputs: | |
| should_publish: ${{ steps.check.outputs.should_publish }} | |
| local_version: ${{ steps.check.outputs.local_version }} | |
| pypi_version: ${{ steps.check.outputs.pypi_version }} | |
| is_prerelease: ${{ steps.check.outputs.is_prerelease }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: ${{ env.DEFAULT_PYTHON_VERSION }} | |
| - name: Check version difference | |
| id: check | |
| run: | | |
| LOCAL_VERSION=$(grep -oP '__version__\s*=\s*"\K[^"]+' src/nanasqlite/__init__.py || echo "0.0.0") | |
| echo "local_version=$LOCAL_VERSION" >> $GITHUB_OUTPUT | |
| echo "Local version: $LOCAL_VERSION" | |
| if [[ "$LOCAL_VERSION" =~ (a|alpha|b|beta|rc|dev) ]]; then | |
| echo "is_prerelease=true" >> $GITHUB_OUTPUT | |
| echo "⚠️ Prerelease version detected" | |
| else | |
| echo "is_prerelease=false" >> $GITHUB_OUTPUT | |
| fi | |
| if curl -s "https://pypi.org/pypi/nanasqlite/${LOCAL_VERSION}/json" 2>/dev/null | grep -q '"version"'; then | |
| PYPI_VERSION="$LOCAL_VERSION" | |
| echo "PyPI version: $PYPI_VERSION (exact match found)" | |
| echo "should_publish=false" >> $GITHUB_OUTPUT | |
| echo "✅ Version $LOCAL_VERSION already on PyPI, skipping publish" | |
| else | |
| PYPI_VERSION=$(curl -s https://pypi.org/pypi/nanasqlite/json 2>/dev/null | grep -oP '"version"\s*:\s*"\K[^"]+' | head -1 || echo "0.0.0") | |
| if [ -z "$PYPI_VERSION" ]; then | |
| PYPI_VERSION="0.0.0" | |
| fi | |
| echo "PyPI version: $PYPI_VERSION (latest)" | |
| echo "should_publish=true" >> $GITHUB_OUTPUT | |
| echo "📦 New version detected: $LOCAL_VERSION" | |
| fi | |
| echo "pypi_version=$PYPI_VERSION" >> $GITHUB_OUTPUT | |
| # ============================================ | |
| # Build - Main branch only, new version only | |
| # ============================================ | |
| build: | |
| name: Build Package | |
| needs: [check-version] | |
| if: needs.check-version.outputs.should_publish == 'true' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| hashes: ${{ steps.hash.outputs.hashes }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| with: | |
| enable-cache: true | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: ${{ env.DEFAULT_PYTHON_VERSION }} | |
| - name: Install build tools | |
| run: uv pip install --system build | |
| - name: Build package | |
| run: python -m build | |
| - name: Generate hashes | |
| id: hash | |
| shell: bash | |
| run: | | |
| cd dist && echo "hashes=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: dist | |
| path: dist/ | |
| retention-days: 7 | |
| # ============================================ | |
| # SLSA Provenance - Main branch only, new version only | |
| # ============================================ | |
| # NOTE: The "provenance / generator" job produces an expected warning: | |
| # "Restore cache failed: Dependencies file is not found ... Supported file pattern: go.sum" | |
| # This comes from the SLSA generator's internal setup-go step, which tries to | |
| # restore a Go module cache using go.sum as the cache key. Since NanaSQLite is | |
| # a pure-Python project, go.sum does not exist and the cache cannot be restored. | |
| # This warning is harmless — SLSA3 provenance is still generated correctly. | |
| # It is an internal limitation of the slsa-github-generator reusable workflow | |
| # and cannot be suppressed from the calling workflow. Tracked upstream: | |
| # https://github.com/slsa-framework/slsa-github-generator/pull/3604 | |
| provenance: | |
| needs: [build] | |
| permissions: | |
| actions: read | |
| id-token: write | |
| contents: write | |
| uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 | |
| with: | |
| base64-subjects: "${{ needs.build.outputs.hashes }}" | |
| # upload-assets is intentionally omitted: this workflow has no tag-based trigger, | |
| # so the SLSA generator would always skip it. The provenance artifact is instead | |
| # downloaded and attached to the GitHub Release in the release job below. | |
| # ============================================ | |
| # Verify SLSA Provenance - Automated check before release | |
| # ============================================ | |
| verify-provenance: | |
| name: Verify SLSA Provenance | |
| needs: [check-version, build, provenance] | |
| if: needs.check-version.outputs.should_publish == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: read | |
| contents: read | |
| steps: | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v8.0.1 | |
| with: | |
| name: dist | |
| path: dist/ | |
| - name: Download provenance | |
| uses: actions/download-artifact@v8.0.1 | |
| with: | |
| name: ${{ needs.provenance.outputs.provenance-name }} | |
| path: provenance-dir/ | |
| - name: Install slsa-verifier | |
| uses: slsa-framework/slsa-verifier/actions/installer@v2.7.1 | |
| - name: Verify provenance | |
| run: | | |
| PROVENANCE_FILE="provenance-dir/${{ needs.provenance.outputs.provenance-name }}" | |
| if [ ! -f "$PROVENANCE_FILE" ]; then | |
| echo "Expected provenance file not found: $PROVENANCE_FILE" | |
| exit 1 | |
| fi | |
| echo "Verifying artifacts against $PROVENANCE_FILE" | |
| for file in dist/*; do | |
| echo "Verifying $file..." | |
| slsa-verifier verify-artifact "$file" \ | |
| --provenance-path "$PROVENANCE_FILE" \ | |
| --source-uri "github.com/${{ github.repository }}" \ | |
| --source-branch "main" | |
| done | |
| # ============================================ | |
| # Publish - Main branch only, new version only | |
| # ============================================ | |
| publish: | |
| name: Publish to PyPI | |
| needs: [check-version, build, verify-provenance] | |
| if: needs.check-version.outputs.should_publish == 'true' | |
| runs-on: ubuntu-latest | |
| environment: pypi | |
| permissions: | |
| id-token: write # Required for PyPI Trusted Publishing. | |
| steps: | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v8.0.1 | |
| with: | |
| name: dist | |
| path: dist/ | |
| # NOTE: The "Generating and uploading digital attestations" notice is expected. | |
| # pypa/gh-action-pypi-publish@release/v1 automatically generates GitHub Artifact | |
| # Attestations (PyPI publish/v1 predicate) when id-token: write is present. | |
| # These are distinct from, and complementary to, the SLSA3 provenance generated | |
| # above. PyPI Trusted Publishing requires these attestations. This is correct | |
| # and desirable behaviour — no action is needed. | |
| - name: Publish to PyPI | |
| uses: pypa/gh-action-pypi-publish@v1.14.0 | |
| # ============================================ | |
| # Release - Main branch only, after publish | |
| # ============================================ | |
| release: | |
| name: GitHub Release | |
| needs: [check-version, build, publish, provenance, verify-provenance] | |
| if: needs.check-version.outputs.should_publish == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v8.0.1 | |
| with: | |
| name: dist | |
| path: dist/ | |
| - name: Download provenance | |
| uses: actions/download-artifact@v8.0.1 | |
| with: | |
| name: ${{ needs.provenance.outputs.provenance-name }} | |
| path: provenance-dir/ | |
| - name: Create source zip | |
| run: | | |
| zip -r nanasqlite-${{ needs.check-version.outputs.local_version }}-src.zip src/ README.md LICENSE pyproject.toml | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| tag_name: v${{ needs.check-version.outputs.local_version }} | |
| name: v${{ needs.check-version.outputs.local_version }} | |
| body: | | |
| ## NanaSQLite v${{ needs.check-version.outputs.local_version }} | |
| ### インストール / Installation | |
| ```bash | |
| pip install nanasqlite==${{ needs.check-version.outputs.local_version }} | |
| ``` | |
| ### ダウンロード / Downloads | |
| - 📦 **wheel**: ビルド済みパッケージ | |
| - 📁 **tar.gz**: ソース配布物 | |
| - 🗂️ **src.zip**: ソースコードのみ | |
| 詳細は [CHANGELOG.md](CHANGELOG.md) を参照してください。 | |
| files: | | |
| dist/* | |
| nanasqlite-${{ needs.check-version.outputs.local_version }}-src.zip | |
| provenance-dir/${{ needs.provenance.outputs.provenance-name }} | |
| draft: false | |
| prerelease: ${{ needs.check-version.outputs.is_prerelease == 'true' }} | |
| - name: Release Summary | |
| run: | | |
| echo "## 🎉 Release Complete" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Version:** v${{ needs.check-version.outputs.local_version }}" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Published to:" >> $GITHUB_STEP_SUMMARY | |
| echo "- ✅ [PyPI](https://pypi.org/project/nanasqlite/${{ needs.check-version.outputs.local_version }}/)" >> $GITHUB_STEP_SUMMARY | |
| echo "- ✅ [GitHub Release](https://github.com/${{ github.repository }}/releases/tag/v${{ needs.check-version.outputs.local_version }})" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY | |
| echo "pip install nanasqlite==${{ needs.check-version.outputs.local_version }}" >> $GITHUB_STEP_SUMMARY | |
| echo "\`\`\`" >> $GITHUB_STEP_SUMMARY | |
| # ============================================ | |
| # PR Comment - PR only, always runs | |
| # 既存コメントがあれば上書き、なければ新規投稿 | |
| # ============================================ | |
| pr-comment: | |
| name: Post PR Comment | |
| needs: [should-run, lint, type, test] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 3 | |
| if: always() && github.event_name == 'pull_request' | |
| permissions: | |
| pull-requests: write | |
| steps: | |
| - name: Post CI results | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| const lint = '${{ needs.lint.result }}'; | |
| const type = '${{ needs.type.result }}'; | |
| const test = '${{ needs.test.result }}'; | |
| const runCi = '${{ needs.should-run.outputs.run_ci }}'; | |
| const icon = (r) => ({ | |
| success: '✅', | |
| skipped: '⏭️', | |
| cancelled: '⚠️', | |
| }[r] ?? '❌'); | |
| const allOk = [lint, type, test].every(r => ['success', 'skipped'].includes(r)); | |
| const header = runCi === 'false' | |
| ? '⏭️ CI skipped (no src/tests changes)' | |
| : allOk | |
| ? '✅ All CI checks passed' | |
| : '❌ One or more CI checks failed'; | |
| const body = [ | |
| `## 🔍 CI Results — ${header}`, | |
| '', | |
| '| Stage | Status |', | |
| '|-------|--------|', | |
| `| 🧹 Lint (ruff) | ${icon(lint)} \`${lint}\` |`, | |
| `| 🔍 Type (mypy) | ${icon(type)} \`${type}\` |`, | |
| `| 🧪 Tests | ${icon(test)} \`${test}\` |`, | |
| '', | |
| `**Commit:** \`${{ github.sha }}\``, | |
| '<!-- ci-results-marker -->', | |
| ].join('\n'); | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| }); | |
| const existing = comments.find(c => c.body.includes('<!-- ci-results-marker -->')); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body, | |
| }); | |
| } |