Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

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