name: E2E Research Test on: pull_request: types: [labeled] # No concurrency group โ€” intentionally omitted. # Previous attempt (#3554, reverted #3599) used cancel-in-progress which # killed in-progress PR runs before they produced useful results. # Future iteration could safely add concurrency for scheduled/push-only # triggers (where head_ref is empty and runs get unique groups). permissions: {} # Minimal top-level for OSSF Scorecard jobs: build-query: name: Assemble query if: github.event.label.name == 'ldr_research' || github.event.label.name == 'ldr_research_static' runs-on: ubuntu-latest permissions: contents: read outputs: query: ${{ steps.assemble.outputs.query }} header: ${{ steps.headers.outputs.header }} subheader: ${{ steps.headers.outputs.subheader }} env: LABEL_NAME: ${{ github.event.label.name }} MAX_DIFF_SIZE: ${{ vars.MAX_DIFF_SIZE || '8000' }} BASE_REF: ${{ github.event.pull_request.base.ref }} steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 with: egress-policy: audit - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false fetch-depth: 0 - name: Build PR-research query id: assemble run: | if [ "$LABEL_NAME" = "ldr_research_static" ]; then cat > query.txt <<'STATIC_EOF' What is Local Deep Research and how does it work? STATIC_EOF else git fetch origin "$BASE_REF" git diff "origin/$BASE_REF...HEAD" --no-color > diff.txt DIFF_BYTES=$(wc -c < diff.txt) echo "Diff size: $DIFF_BYTES bytes (cap: $MAX_DIFF_SIZE)" head -c "$MAX_DIFF_SIZE" diff.txt > diff-trunc.txt if [ "$DIFF_BYTES" -gt "$MAX_DIFF_SIZE" ]; then printf '\n... (truncated)\n' >> diff-trunc.txt fi { cat <<'PROMPT_HEAD_EOF' Based on these code changes, research relevant documentation, best practices, and potential issues. Focus on any libraries, APIs, or patterns used. Code changes: PROMPT_HEAD_EOF cat diff-trunc.txt cat <<'PROMPT_TAIL_EOF' Research topics to cover: 1. Documentation for any libraries or APIs being used/modified 2. Best practices for the patterns shown 3. Known issues or gotchas related to these changes 4. Security considerations if applicable PROMPT_TAIL_EOF } > query.txt fi # Multi-line GH Actions output via heredoc-style delimiter. # Use a randomized delimiter so a query containing the literal # delimiter on its own line cannot prematurely terminate the # heredoc. DELIM="LDR_QUERY_EOF_$$_${RANDOM}_$(date +%N)" { echo "query<<$DELIM" cat query.txt echo "$DELIM" } >> "$GITHUB_OUTPUT" - name: Compute headers id: headers run: | if [ "$LABEL_NAME" = "ldr_research_static" ]; then echo "header=## ๐Ÿงช LDR Static Query Test Results" >> "$GITHUB_OUTPUT" echo "subheader=**Query:** What is Local Deep Research and how does it work?" >> "$GITHUB_OUTPUT" else echo "header=## ๐Ÿ”ฌ LDR Research Results" >> "$GITHUB_OUTPUT" echo "subheader=_Analysis of PR code changes_" >> "$GITHUB_OUTPUT" fi research: name: Run LDR research needs: build-query # Grant the perms the reusable's job needs: # - contents: read for actions/checkout # - actions: write for actions/upload-artifact@v5+ # Without this, the reusable's `permissions: contents: read` exceeds # the inherited empty permissions and GitHub rejects the workflow at # load time (startup_failure with zero jobs). permissions: contents: read actions: write uses: ./.github/workflows/ldr-research-reusable.yml with: query: ${{ needs.build-query.outputs.query }} model: ${{ github.event.label.name == 'ldr_research_static' && (vars.LDR_RESEARCH_CHEAP_MODEL || 'google/gemini-2.0-flash-001') || (vars.LDR_RESEARCH_MODEL || 'google/gemini-2.0-flash-001') }} provider: ${{ vars.LDR_PROVIDER || 'openrouter' }} search-tool: ${{ vars.LDR_SEARCH_TOOL || 'serper' }} strategy: ${{ vars.LDR_STRATEGY || 'langgraph-agent' }} comment-header: ${{ needs.build-query.outputs.header }} comment-subheader: ${{ needs.build-query.outputs.subheader }} secrets: OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} SERPER_API_KEY: ${{ secrets.SERPER_API_KEY }} post-comment: name: Post PR comment needs: research # always() so the label-removal step runs even if research was skipped # (e.g. build-query failed). The download/post steps are guarded by # needs.research.outputs.success == 'true' so they self-skip in that # case. if: always() runs-on: ubuntu-latest permissions: contents: read pull-requests: write issues: write env: LABEL_NAME: ${{ github.event.label.name }} PR_NUMBER: ${{ github.event.pull_request.number }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 with: egress-policy: audit - name: Download research artifact if: needs.research.outputs.success == 'true' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ needs.research.outputs.comment-artifact-name }} - name: Post or update comment if: needs.research.outputs.success == 'true' continue-on-error: true # a GitHub API hiccup shouldn't fail the job uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: retries: 3 script: | const fs = require('fs'); const issue_number = Number(process.env.PR_NUMBER); // Key the marker by label so re-running the same label edits its // comment in place, while the other label keeps a separate comment. const marker = ``; const body = marker + '\n' + fs.readFileSync('comment.md', 'utf8'); // Paginate โ€” on a long-lived PR the existing comment may be past the // first page (listComments defaults to 30), which would otherwise // create a new comment every run instead of editing in place. const comments = await github.paginate(github.rest.issues.listComments, { owner: context.repo.owner, repo: context.repo.repo, issue_number, per_page: 100, }); const existing = comments.find( (c) => c.user.type === 'Bot' && c.body.includes(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, body, }); } - name: Remove label for re-triggering if: always() run: | gh issue edit "$PR_NUMBER" --remove-label "$LABEL_NAME" 2>/dev/null || true