mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
feat: daily docs-sync bot keeping kilo-docs in sync with merged PRs (#12512)
* feat: daily docs-sync bot workflow (Kilo CLI) Adds a scheduled workflow that keeps packages/kilo-docs in sync with PRs merged to Kilo-Org/cloud and Kilo-Org/kilocode: - watermark.mjs derives the processing window from the bot's own PR body marker (self-healing, no external state; 72h fallback, 14d cap) - collect.mjs queries merged PRs via the GitHub API and applies a deterministic pre-filter (bots, chores, docs-only PRs) - triage.mjs classifies PRs in chunks of 25 with kilo run; failed chunks degrade to unclassified instead of failing the run - edit.mjs updates docs in batches of 5 PRs with kilo run, bounded per batch; failures surface as skipped entries in the PR body - verify runs the kilo-docs build + test suite; one LLM fix pass on failure; still-red becomes a draft PR - upsert-pr.mjs maintains one rolling auto-docs PR (appends while open, fresh branch after merge), with a 15-file draft cap and a machine-readable processed-through watermark Also adds docs-sync.yml to the workflow allowlist in script/check-workflows.ts. * fix: correct kilo run invocation and auth - message positional must come before flags: --file is multi-value and consumes a trailing message as a file path (File not found) - authenticate via the existing KILO_API_KEY repo secret (the kilo provider reads it natively); drop the DOCS_SYNC_KILO_CONFIG config secret requirement - fix default model IDs: gateway provider id is kilo/, not kilocode/ - include stderr tail in triage/edit failure logs * fix: handle kilo run double-printed assistant output kilo run prints the assistant message twice (streaming render + final summary), so stdout can contain the same JSON array back-to-back. Parse the largest valid trailing array instead of slicing first-to-last bracket. Verified against real chunked triage output. * fix: reviewer-pass robustness fixes - edit.mjs: unambiguous summary file path in the batch prompt and a fallback read when the agent drops the docs-sync-out/ prefix, so real edits never report as skipped - prepare-branch.mjs: use the open auto-docs PR's actual head.ref instead of assuming docs/auto-sync - upsert-pr.mjs: compute the 15-file draft cap on the cumulative PR diff (origin/main...HEAD), not just the latest commit * fix: address Kilobot review findings Security: - sanitize HTML-comment sequences out of agent-generated PR body values so a crafted value cannot forge section markers or the watermark - draft any PR whose diff touches non-content files in packages/kilo-docs (outside pages/ and lib/nav/) — build-executable changes force human review before merge - on merge conflict, keep the conflicted rolling branch untouched (preserving human commits) and continue on a fresh dated branch that links the old PR Resilience: - retry GitHub API calls on network errors and 5xx, not just 403 rate limits - isolate per-PR collect failures instead of aborting the run - trust watermark markers only on bot-authored PRs and clamp future dates loudly - validate chunk triage entries belong to their chunk before the shared dedupe - use changed_files for files_total and skip docs-only classification on truncated (300+) file lists - pipe stderr in the edit pass so failure warnings carry the real CLI error * fix: address second Kilobot review round - escape pipe characters in changeRow actions (same as skippedRow) - sanitize agent-chosen file paths before they land in draftReasons and the PR body (residual marker-forgery path via filenames) - log expected fetch misses in prepare-branch instead of silent catches * feat: keep bot-authored PRs in the docs-sync digest Release and dependency bots ship user-facing changes (e.g. JetBrains release PRs from kilo-maintainer[bot]). The auto-docs label check and docs-only path filter remain as the loop guards.
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
// kilocode_change - new file
|
||||
|
||||
/**
|
||||
* Shared helpers for the docs-sync bot scripts. Dependency-free (Node 20+
|
||||
* global fetch) so the workflow does not rely on runner images shipping the
|
||||
* gh CLI.
|
||||
*/
|
||||
|
||||
import fs from "node:fs"
|
||||
|
||||
const API = "https://api.github.com"
|
||||
const MAX_RETRIES = 3
|
||||
|
||||
export function token() {
|
||||
const t = process.env.GH_TOKEN || process.env.GITHUB_TOKEN
|
||||
if (!t) throw new Error("GH_TOKEN (or GITHUB_TOKEN) is required")
|
||||
return t
|
||||
}
|
||||
|
||||
export function repo() {
|
||||
const r = process.env.GITHUB_REPOSITORY
|
||||
if (!r) throw new Error("GITHUB_REPOSITORY is required")
|
||||
return r
|
||||
}
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
||||
|
||||
export async function api(path, { method = "GET", body } = {}) {
|
||||
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||
let res
|
||||
try {
|
||||
res = await fetch(`${API}${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
authorization: `Bearer ${token()}`,
|
||||
accept: "application/vnd.github+json",
|
||||
"x-github-api-version": "2022-11-28",
|
||||
"user-agent": "kilo-docs-sync-bot",
|
||||
},
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
})
|
||||
} catch (err) {
|
||||
if (attempt < MAX_RETRIES) {
|
||||
console.warn(`network error (${err.message}), retrying in ${5 * attempt}s`)
|
||||
await sleep(5000 * attempt)
|
||||
continue
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
if (res.status === 403) {
|
||||
const text = await res.text()
|
||||
if (text.includes("rate limit") && attempt < MAX_RETRIES) {
|
||||
const retryAfter = Number(res.headers.get("retry-after")) || 30
|
||||
console.warn(`rate limited, retrying in ${retryAfter}s`)
|
||||
await sleep(retryAfter * 1000)
|
||||
continue
|
||||
}
|
||||
const err = new Error(`${method} ${path} -> 403: ${text}`)
|
||||
err.status = 403
|
||||
throw err
|
||||
}
|
||||
|
||||
if (res.status >= 500 && attempt < MAX_RETRIES) {
|
||||
console.warn(`${method} ${path} -> ${res.status}, retrying in ${5 * attempt}s`)
|
||||
await sleep(5000 * attempt)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text()
|
||||
const err = new Error(`${method} ${path} -> ${res.status}: ${text}`)
|
||||
err.status = res.status
|
||||
throw err
|
||||
}
|
||||
|
||||
if (res.status === 204) return null
|
||||
return res.json()
|
||||
}
|
||||
throw new Error(`${method} ${path}: exhausted retries`)
|
||||
}
|
||||
|
||||
/** Paginated search/issues. Caps at `maxPages` * 100 results. */
|
||||
export async function searchIssues(query, { maxPages = 5 } = {}) {
|
||||
const items = []
|
||||
for (let page = 1; page <= maxPages; page++) {
|
||||
const data = await api(`/search/issues?q=${encodeURIComponent(query)}&per_page=100&page=${page}`)
|
||||
items.push(...(data.items ?? []))
|
||||
if ((data.items ?? []).length < 100) break
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
export async function listPrFiles(fullRepo, number, { maxPages = 3 } = {}) {
|
||||
const files = []
|
||||
for (let page = 1; page <= maxPages; page++) {
|
||||
const batch = await api(`/repos/${fullRepo}/pulls/${number}/files?per_page=100&page=${page}`)
|
||||
files.push(...batch)
|
||||
if (batch.length < 100) break
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
export function appendOutput(name, value) {
|
||||
const out = process.env.GITHUB_OUTPUT
|
||||
if (out) fs.appendFileSync(out, `${name}=${value}\n`)
|
||||
console.log(`output ${name}=${value}`)
|
||||
}
|
||||
|
||||
export function appendSummary(markdown) {
|
||||
const summary = process.env.GITHUB_STEP_SUMMARY
|
||||
if (summary) fs.appendFileSync(summary, markdown + "\n")
|
||||
}
|
||||
Reference in New Issue
Block a user