7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
301 lines
11 KiB
YAML
301 lines
11 KiB
YAML
name: LDR Research (reusable)
|
|
|
|
# Reusable workflow that runs the LDR research script on a caller-supplied
|
|
# query and produces a comment-ready markdown blob (uploaded as an
|
|
# artifact). Callers download the artifact and post it to their target
|
|
# (PR comment, issue comment, Reddit, etc.). This workflow does NOT post
|
|
# anywhere — that's caller-specific.
|
|
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
query:
|
|
description: 'Fully-assembled research prompt. The caller does all prompt engineering.'
|
|
type: string
|
|
required: true
|
|
model:
|
|
description: 'OpenRouter model slug. Empty falls back to vars.LDR_RESEARCH_MODEL then a hard default.'
|
|
type: string
|
|
default: ''
|
|
provider:
|
|
description: 'LLM provider.'
|
|
type: string
|
|
default: 'openrouter'
|
|
search-tool:
|
|
description: 'Search tool.'
|
|
type: string
|
|
default: 'serper'
|
|
strategy:
|
|
description: 'LDR search strategy.'
|
|
type: string
|
|
default: 'langgraph-agent'
|
|
iterations:
|
|
description: 'Override the strategy iteration cap. 0 keeps the strategy default.'
|
|
type: number
|
|
default: 0
|
|
max-query-length:
|
|
description: 'Backstop truncation on the query string before invoking the script.'
|
|
type: number
|
|
default: 12000
|
|
max-sources:
|
|
description: 'Cap sources rendered in the markdown.'
|
|
type: number
|
|
default: 10
|
|
comment-header:
|
|
description: 'Top header line of the formatted markdown.'
|
|
type: string
|
|
default: '## LDR Research Results'
|
|
comment-subheader:
|
|
description: 'Optional second-line subheader.'
|
|
type: string
|
|
default: ''
|
|
comment-footer:
|
|
description: 'Footer line. Reddit will swap in a bot disclaimer.'
|
|
type: string
|
|
default: '_Generated by [Local Deep Research](https://github.com/LearningCircuit/local-deep-research) E2E test_'
|
|
include-sources-section:
|
|
description: 'Render a dedicated sources section. Reddit will set false to fit length caps.'
|
|
type: boolean
|
|
default: true
|
|
output-truncate-chars:
|
|
description: 'Truncate the rendered markdown to this many chars. 0 disables truncation.'
|
|
type: number
|
|
default: 0
|
|
runner:
|
|
description: 'Runner label.'
|
|
type: string
|
|
default: 'ubuntu-latest'
|
|
artifact-suffix:
|
|
description: 'Optional disambiguator appended to the artifact name. Use a unique value per matrix entry when calling this workflow multiple times in one caller run.'
|
|
type: string
|
|
default: ''
|
|
secrets:
|
|
OPENROUTER_API_KEY:
|
|
required: true
|
|
SERPER_API_KEY:
|
|
required: true
|
|
outputs:
|
|
comment-artifact-name:
|
|
description: 'Name of the artifact containing comment.md and response.json.'
|
|
value: ${{ jobs.research.outputs.comment-artifact-name }}
|
|
success:
|
|
description: "'true' if LDR returned valid non-error JSON."
|
|
value: ${{ jobs.research.outputs.success }}
|
|
|
|
permissions: {}
|
|
|
|
jobs:
|
|
research:
|
|
name: Run LDR research
|
|
runs-on: ${{ inputs.runner }}
|
|
permissions:
|
|
contents: read
|
|
outputs:
|
|
comment-artifact-name: ${{ steps.artifact-name.outputs.name }}
|
|
success: ${{ steps.run-script.outputs.success }}
|
|
env:
|
|
QUERY: ${{ inputs.query }}
|
|
MAX_QUERY_LENGTH: ${{ inputs.max-query-length }}
|
|
MAX_SOURCES: ${{ inputs.max-sources }}
|
|
COMMENT_HEADER: ${{ inputs.comment-header }}
|
|
COMMENT_SUBHEADER: ${{ inputs.comment-subheader }}
|
|
COMMENT_FOOTER: ${{ inputs.comment-footer }}
|
|
INCLUDE_SOURCES: ${{ inputs.include-sources-section }}
|
|
OUTPUT_TRUNCATE_CHARS: ${{ inputs.output-truncate-chars }}
|
|
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: 1
|
|
|
|
- name: Install jq
|
|
run: sudo apt-get update && sudo apt-get install -y jq
|
|
|
|
- name: Compute artifact name
|
|
id: artifact-name
|
|
env:
|
|
SUFFIX: ${{ inputs.artifact-suffix }}
|
|
run: |
|
|
NAME="ldr-research-${{ github.run_id }}-${{ github.run_attempt }}-${{ github.job }}"
|
|
if [ -n "$SUFFIX" ]; then
|
|
# Sanitize: artifact names allow [A-Za-z0-9._-] only.
|
|
SAFE_SUFFIX=$(printf '%s' "$SUFFIX" | tr -c 'A-Za-z0-9._-' '_')
|
|
NAME="${NAME}-${SAFE_SUFFIX}"
|
|
fi
|
|
echo "name=$NAME" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Resolve model
|
|
id: resolve-model
|
|
env:
|
|
INPUT_MODEL: ${{ inputs.model }}
|
|
REPO_VAR_MODEL: ${{ vars.LDR_RESEARCH_MODEL }}
|
|
run: |
|
|
if [ -n "$INPUT_MODEL" ]; then
|
|
echo "model=$INPUT_MODEL" >> "$GITHUB_OUTPUT"
|
|
elif [ -n "$REPO_VAR_MODEL" ]; then
|
|
echo "model=$REPO_VAR_MODEL" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "model=google/gemini-2.0-flash-001" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Set up PDM
|
|
uses: pdm-project/setup-pdm@973541a5febeafcfdadf8a51211435be6ecfd90f # v4.5
|
|
with:
|
|
python-version: '3.12'
|
|
cache: true
|
|
|
|
- name: Install LDR
|
|
run: pdm install
|
|
|
|
- name: Write query to file (with backstop truncation)
|
|
run: |
|
|
# Truncate by total byte count, not per-line. Avoids putting the
|
|
# entire query on the command line where it could exceed ARG_MAX.
|
|
QUERY_BYTES=$(printf '%s' "$QUERY" | wc -c)
|
|
if [ "$QUERY_BYTES" -gt "$MAX_QUERY_LENGTH" ]; then
|
|
printf '%s' "$QUERY" | head -c "$MAX_QUERY_LENGTH" > query.txt
|
|
printf '\n... (truncated)\n' >> query.txt
|
|
echo "Query truncated: $QUERY_BYTES -> $MAX_QUERY_LENGTH bytes"
|
|
else
|
|
printf '%s' "$QUERY" > query.txt
|
|
fi
|
|
echo "Final query size: $(wc -c < query.txt) bytes"
|
|
|
|
- name: Run LDR Research
|
|
id: run-script
|
|
env:
|
|
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
|
SERPER_API_KEY: ${{ secrets.SERPER_API_KEY }}
|
|
LDR_PROVIDER: ${{ inputs.provider }}
|
|
LDR_SEARCH_TOOL: ${{ inputs.search-tool }}
|
|
LDR_RESEARCH_MODEL: ${{ steps.resolve-model.outputs.model }}
|
|
LDR_STRATEGY: ${{ inputs.strategy }}
|
|
ITERATIONS: ${{ inputs.iterations }}
|
|
run: |
|
|
set +e
|
|
if [ "$ITERATIONS" -gt 0 ] 2>/dev/null; then
|
|
pdm run python scripts/ldr-research.py --iterations "$ITERATIONS" \
|
|
< query.txt 2> >(tee stderr.log >&2) > response.json
|
|
else
|
|
pdm run python scripts/ldr-research.py \
|
|
< query.txt 2> >(tee stderr.log >&2) > response.json
|
|
fi
|
|
LDR_EXIT_CODE=$?
|
|
set -e
|
|
|
|
echo "=== Response (first 2000 chars): ==="
|
|
head -c 2000 response.json
|
|
echo ""
|
|
echo "=== End response ==="
|
|
|
|
# Catches SIGABRT/native crashes where stdout never flushed; JSON check alone can't see this.
|
|
if [ "$LDR_EXIT_CODE" -ne 0 ]; then
|
|
LAST_ERR=$(tail -c 500 stderr.log 2>/dev/null || echo "")
|
|
echo "::error::ldr-research.py exited with code $LDR_EXIT_CODE: $LAST_ERR"
|
|
echo "success=false" >> "$GITHUB_OUTPUT"
|
|
exit 1
|
|
fi
|
|
|
|
# `jq .` exits 0 on a zero-byte file, so guard explicitly.
|
|
if [ ! -s response.json ]; then
|
|
echo "::error::response.json is empty"
|
|
echo "success=false" >> "$GITHUB_OUTPUT"
|
|
exit 1
|
|
fi
|
|
|
|
# Shape validation: must be a JSON object.
|
|
if ! jq -e 'type == "object"' response.json > /dev/null 2>&1; then
|
|
echo "::error::Response is not a JSON object"
|
|
echo "success=false" >> "$GITHUB_OUTPUT"
|
|
exit 1
|
|
fi
|
|
|
|
ERROR=$(jq -r '.error // empty' response.json)
|
|
if [ -n "$ERROR" ]; then
|
|
echo "::error::LDR error: $ERROR"
|
|
echo "success=false" >> "$GITHUB_OUTPUT"
|
|
exit 1
|
|
fi
|
|
|
|
# Empty .research would still produce a hollow downstream comment.
|
|
RESEARCH=$(jq -r '.research // empty' response.json)
|
|
if [ -z "$RESEARCH" ]; then
|
|
echo "::error::Response missing or empty .research field"
|
|
echo "success=false" >> "$GITHUB_OUTPUT"
|
|
exit 1
|
|
fi
|
|
|
|
echo "success=true" >> "$GITHUB_OUTPUT"
|
|
echo "✅ Research completed"
|
|
|
|
- name: Build comment markdown
|
|
run: |
|
|
jq -r '.research' response.json > result.md
|
|
|
|
# Optional sources section
|
|
if [ "$INCLUDE_SOURCES" = "true" ]; then
|
|
SOURCES=$(jq -r --argjson cap "$MAX_SOURCES" \
|
|
'.sources[:$cap][] | "- [\(.title // "Source")](\(.link // .url // ""))"' \
|
|
response.json 2>/dev/null || echo "")
|
|
else
|
|
SOURCES=""
|
|
fi
|
|
|
|
{
|
|
echo "$COMMENT_HEADER"
|
|
echo ""
|
|
if [ -n "$COMMENT_SUBHEADER" ]; then
|
|
echo "$COMMENT_SUBHEADER"
|
|
echo ""
|
|
fi
|
|
cat result.md
|
|
echo ""
|
|
if [ -n "$SOURCES" ]; then
|
|
echo "### 🔍 Search Sources"
|
|
echo ""
|
|
echo "$SOURCES"
|
|
echo ""
|
|
fi
|
|
echo "---"
|
|
echo "$COMMENT_FOOTER"
|
|
} > comment.md
|
|
|
|
# Optional truncation (Reddit will use this)
|
|
if [ "$OUTPUT_TRUNCATE_CHARS" -gt 0 ] 2>/dev/null; then
|
|
CURRENT=$(wc -c < comment.md)
|
|
if [ "$CURRENT" -gt "$OUTPUT_TRUNCATE_CHARS" ]; then
|
|
# Truncate at last paragraph break before the limit, with a budget
|
|
# for the trailing marker.
|
|
MARKER='…[truncated]'
|
|
BUDGET=$((OUTPUT_TRUNCATE_CHARS - ${#MARKER} - 1))
|
|
head -c "$BUDGET" comment.md > comment.md.tmp
|
|
# Trim trailing partial line so we end on a clean paragraph
|
|
awk 'BEGIN{RS=""} {gsub(/[[:space:]]+$/,""); print}' \
|
|
comment.md.tmp > comment.md
|
|
echo "" >> comment.md
|
|
echo "$MARKER" >> comment.md
|
|
rm -f comment.md.tmp
|
|
echo "Truncated comment.md to $OUTPUT_TRUNCATE_CHARS chars."
|
|
fi
|
|
fi
|
|
|
|
echo "Final comment.md size: $(wc -c < comment.md) bytes"
|
|
|
|
- name: Upload research artifact
|
|
if: always()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: ${{ steps.artifact-name.outputs.name }}
|
|
path: |
|
|
comment.md
|
|
response.json
|
|
stderr.log
|
|
if-no-files-found: ignore
|
|
retention-days: 7
|