feat(git): 优化状态与日志控制 #54
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: Lithe PR review | |
| on: | |
| issue_comment: | |
| types: [created] | |
| permissions: | |
| contents: read | |
| env: | |
| LITHE_CODEX_MODEL: gpt-5.6-sol | |
| jobs: | |
| prepare: | |
| if: >- | |
| github.event.issue.pull_request != null && | |
| github.event.comment.body == '@lithe review' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| issues: write | |
| pull-requests: write | |
| outputs: | |
| authorized: ${{ steps.prepare.outputs.authorized }} | |
| base_sha: ${{ steps.prepare.outputs.base_sha }} | |
| head_sha: ${{ steps.prepare.outputs.head_sha }} | |
| review_mode: ${{ steps.prepare.outputs.review_mode }} | |
| steps: | |
| - name: Check out trusted review tooling | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ github.event.repository.default_branch }} | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "24" | |
| - name: Test review context builder | |
| run: node --test scripts/test-prepare-lithe-*review*.mjs | |
| - name: Prepare trusted review context | |
| id: prepare | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| LITHE_ALLOWED_REVIEWERS: ${{ vars.LITHE_ALLOWED_REVIEWERS }} | |
| LITHE_PROMPT_OUTPUT: ${{ runner.temp }}/lithe-pr-review-prompt.md | |
| run: node scripts/prepare-lithe-pr-review.mjs | |
| - name: Publish review acknowledgement | |
| if: steps.prepare.outputs.authorized == 'true' | |
| uses: actions/github-script@v7 | |
| env: | |
| LITHE_HEAD_SHA: ${{ steps.prepare.outputs.head_sha }} | |
| LITHE_REVIEW_MODE: ${{ steps.prepare.outputs.review_mode }} | |
| with: | |
| github-token: ${{ github.token }} | |
| script: | | |
| const marker = `<!-- lithe-review:${context.issue.number}:${process.env.LITHE_HEAD_SHA} -->`; | |
| const mode = process.env.LITHE_REVIEW_MODE === 'large' ? '大型变更' : '标准'; | |
| const body = `${marker}\n## Lithe Review\n\n⏳ 正在以${mode}模式审查,完成或失败后会更新本评论。`; | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| per_page: 100, | |
| }); | |
| const previous = comments.find((comment) => | |
| comment.user?.login === 'github-actions[bot]' && comment.body?.startsWith(marker) | |
| ); | |
| if (previous) { | |
| await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: previous.id, body }); | |
| } else { | |
| await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body }); | |
| } | |
| - name: Upload trusted review input | |
| if: steps.prepare.outputs.authorized == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: lithe-review-input-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: ${{ runner.temp }}/lithe-pr-review-prompt.md | |
| if-no-files-found: error | |
| retention-days: 1 | |
| review: | |
| needs: prepare | |
| if: needs.prepare.outputs.authorized == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| concurrency: | |
| group: lithe-pr-review-${{ github.repository }}-${{ github.event.issue.number }}-${{ needs.prepare.outputs.head_sha }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read | |
| outputs: | |
| final_message: ${{ steps.finalize.outputs.final_message }} | |
| review_outcome: ${{ steps.finalize.outputs.review_outcome }} | |
| steps: | |
| - name: Check out trusted review tooling | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ github.event.repository.default_branch }} | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - name: Stage trusted review tooling | |
| env: | |
| LITHE_TOOLING: ${{ runner.temp }}/lithe-review-tooling | |
| run: | | |
| set -euo pipefail | |
| mkdir -p "$LITHE_TOOLING" | |
| install -m 755 scripts/prepare-lithe-review-stage.mjs "$LITHE_TOOLING/stage.mjs" | |
| install -m 600 .github/lithe-review/final.schema.json "$LITHE_TOOLING/final.schema.json" | |
| - name: Download trusted review input | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: lithe-review-input-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: ${{ runner.temp }}/lithe-review-input | |
| - name: Check out the complete pull request head | |
| uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ needs.prepare.outputs.head_sha }} | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Verify checked-out revision | |
| env: | |
| LITHE_BASE_SHA: ${{ needs.prepare.outputs.base_sha }} | |
| LITHE_HEAD_SHA: ${{ needs.prepare.outputs.head_sha }} | |
| run: | | |
| set -euo pipefail | |
| test "$(git rev-parse HEAD)" = "$LITHE_HEAD_SHA" | |
| git cat-file -e "${LITHE_BASE_SHA}^{commit}" | |
| - name: Review pull request with Codex | |
| id: codex | |
| continue-on-error: true | |
| uses: openai/codex-action@v1 | |
| with: | |
| openai-api-key: ${{ secrets.LITHE_CODEX_API_KEY }} | |
| responses-api-endpoint: ${{ secrets.LITHE_CODEX_RESPONSES_URL }} | |
| prompt-file: ${{ runner.temp }}/lithe-review-input/lithe-pr-review-prompt.md | |
| output-file: ${{ runner.temp }}/lithe-review-final-candidate.json | |
| output-schema-file: ${{ runner.temp }}/lithe-review-tooling/final.schema.json | |
| model: ${{ env.LITHE_CODEX_MODEL }} | |
| effort: medium | |
| sandbox: read-only | |
| safety-strategy: drop-sudo | |
| allow-users: ${{ github.actor }} | |
| - name: Finalize review output | |
| if: always() | |
| id: finalize | |
| run: >- | |
| node "${{ runner.temp }}/lithe-review-tooling/stage.mjs" finalize | |
| --candidate "${{ runner.temp }}/lithe-review-final-candidate.json" | |
| --output "${{ runner.temp }}/lithe-review-final.md" | |
| - name: Upload review diagnostics | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: lithe-review-${{ github.event.issue.number }}-${{ github.run_attempt }} | |
| path: | | |
| ${{ runner.temp }}/lithe-review-final-candidate.json | |
| ${{ runner.temp }}/lithe-review-final.md | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| - name: Report review failure | |
| if: always() && steps.finalize.outputs.review_outcome != 'success' | |
| run: | | |
| echo "Codex review did not produce a valid final result" >&2 | |
| exit 1 | |
| publish: | |
| needs: [prepare, review] | |
| if: >- | |
| always() && | |
| needs.prepare.outputs.authorized == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: | |
| issues: write | |
| pull-requests: write | |
| steps: | |
| - name: Publish or update the review | |
| uses: actions/github-script@v7 | |
| env: | |
| LITHE_FINAL_MESSAGE: ${{ needs.review.outputs.final_message }} | |
| LITHE_HEAD_SHA: ${{ needs.prepare.outputs.head_sha }} | |
| LITHE_REVIEW_OUTCOME: ${{ needs.review.outputs.review_outcome }} | |
| LITHE_REVIEW_JOB_RESULT: ${{ needs.review.result }} | |
| LITHE_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| with: | |
| github-token: ${{ github.token }} | |
| script: | | |
| const marker = `<!-- lithe-review:${context.issue.number}:${process.env.LITHE_HEAD_SHA} -->`; | |
| const message = (process.env.LITHE_FINAL_MESSAGE ?? '').trim(); | |
| const limit = 65_000; | |
| const failureMessage = [ | |
| '## Lithe Review', | |
| '', | |
| '**结论:** ❓ 审查未完成', | |
| '', | |
| `审查状态:\`${process.env.LITHE_REVIEW_OUTCOME || process.env.LITHE_REVIEW_JOB_RESULT}\`。`, | |
| `请查看[本次 Actions 日志与诊断附件](${process.env.LITHE_RUN_URL})后重新召唤。`, | |
| ].join('\n'); | |
| const body = `${marker}\n${message || failureMessage}`.slice(0, limit); | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| per_page: 100, | |
| }); | |
| const previous = comments.find((comment) => | |
| comment.user?.login === 'github-actions[bot]' && comment.body?.startsWith(marker) | |
| ); | |
| if (previous) { | |
| await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: previous.id, body }); | |
| } else { | |
| await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body }); | |
| } |