bosun 0.40.12 → 0.40.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.mjs +97 -0
- package/infra/monitor.mjs +93 -1
- package/package.json +1 -1
- package/ui/demo-defaults.js +2 -2
- package/workflow/workflow-nodes.mjs +58 -4
- package/workflow-templates/github.mjs +19 -4
package/cli.mjs
CHANGED
|
@@ -336,6 +336,19 @@ const DAEMON_MAX_INSTANT_RESTARTS = Math.max(
|
|
|
336
336
|
1,
|
|
337
337
|
Number(process.env.BOSUN_DAEMON_MAX_INSTANT_RESTARTS || 5) || 5,
|
|
338
338
|
);
|
|
339
|
+
const DAEMON_MISCONFIG_GUARD_ENABLED = !["0", "false", "off", "no"].includes(
|
|
340
|
+
String(process.env.BOSUN_DAEMON_MISCONFIG_GUARD || "1")
|
|
341
|
+
.trim()
|
|
342
|
+
.toLowerCase(),
|
|
343
|
+
);
|
|
344
|
+
const DAEMON_MISCONFIG_GUARD_MIN_RESTARTS = Math.max(
|
|
345
|
+
1,
|
|
346
|
+
Number(process.env.BOSUN_DAEMON_MISCONFIG_GUARD_MIN_RESTARTS || 3) || 3,
|
|
347
|
+
);
|
|
348
|
+
const DAEMON_MISCONFIG_LOG_SCAN_LINES = Math.max(
|
|
349
|
+
20,
|
|
350
|
+
Number(process.env.BOSUN_DAEMON_MISCONFIG_LOG_SCAN_LINES || 250) || 250,
|
|
351
|
+
);
|
|
339
352
|
let daemonRestartCount = 0;
|
|
340
353
|
const daemonCrashTracker = createDaemonCrashTracker({
|
|
341
354
|
instantCrashWindowMs: DAEMON_INSTANT_CRASH_WINDOW_MS,
|
|
@@ -2080,6 +2093,77 @@ function getMonitorPidFileCandidates(extraCacheDirs = []) {
|
|
|
2080
2093
|
]);
|
|
2081
2094
|
}
|
|
2082
2095
|
|
|
2096
|
+
function tailLinesFromFile(filePath, maxLines = 200) {
|
|
2097
|
+
try {
|
|
2098
|
+
if (!existsSync(filePath)) return [];
|
|
2099
|
+
const raw = readFileSync(filePath, "utf8");
|
|
2100
|
+
if (!raw) return [];
|
|
2101
|
+
const lines = raw.split(/\r?\n/).filter(Boolean);
|
|
2102
|
+
if (lines.length <= maxLines) return lines;
|
|
2103
|
+
return lines.slice(-maxLines);
|
|
2104
|
+
} catch {
|
|
2105
|
+
return [];
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
function detectDaemonRestartStormSignals(options) {
|
|
2110
|
+
const resolvedOptions = options && typeof options === "object" ? options : {};
|
|
2111
|
+
const logDir = resolvedOptions.logDir || resolve(__dirname, "logs");
|
|
2112
|
+
const maxLines = resolvedOptions.maxLines || DAEMON_MISCONFIG_LOG_SCAN_LINES;
|
|
2113
|
+
const reasons = [];
|
|
2114
|
+
const monitorErrorLines = tailLinesFromFile(
|
|
2115
|
+
resolve(logDir, "monitor-error.log"),
|
|
2116
|
+
maxLines,
|
|
2117
|
+
);
|
|
2118
|
+
const monitorLines = tailLinesFromFile(resolve(logDir, "monitor.log"), maxLines);
|
|
2119
|
+
const combined = [...monitorErrorLines, ...monitorLines].join("\n");
|
|
2120
|
+
if (!combined) {
|
|
2121
|
+
return { hasSignal: false, reasons: [] };
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
if (
|
|
2125
|
+
/missing prerequisites:\s*no API key|codex unavailable:\s*no API key/i.test(
|
|
2126
|
+
combined,
|
|
2127
|
+
)
|
|
2128
|
+
) {
|
|
2129
|
+
reasons.push("missing_api_key");
|
|
2130
|
+
}
|
|
2131
|
+
if (
|
|
2132
|
+
/another bosun instance holds the lock|duplicate start ignored|another bosun is already running/i
|
|
2133
|
+
.test(combined)
|
|
2134
|
+
) {
|
|
2135
|
+
reasons.push("duplicate_runtime");
|
|
2136
|
+
}
|
|
2137
|
+
if (/Shared state heartbeat FATAL.*owner_mismatch/i.test(combined)) {
|
|
2138
|
+
reasons.push("shared_state_owner_mismatch");
|
|
2139
|
+
}
|
|
2140
|
+
if (
|
|
2141
|
+
/There is no tracking information for the current branch|git pull <remote> <branch>/i
|
|
2142
|
+
.test(combined)
|
|
2143
|
+
) {
|
|
2144
|
+
reasons.push("workspace_git_tracking_missing");
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
return {
|
|
2148
|
+
hasSignal: reasons.length > 0,
|
|
2149
|
+
reasons,
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
function shouldPauseDaemonRestartStorm(options) {
|
|
2154
|
+
const resolvedOptions = options && typeof options === "object" ? options : {};
|
|
2155
|
+
const restartCount = Number(resolvedOptions.restartCount || 0);
|
|
2156
|
+
const logDir = resolvedOptions.logDir;
|
|
2157
|
+
if (!IS_DAEMON_CHILD) return { pause: false, reasons: [] };
|
|
2158
|
+
if (!DAEMON_MISCONFIG_GUARD_ENABLED) return { pause: false, reasons: [] };
|
|
2159
|
+
if (restartCount < DAEMON_MISCONFIG_GUARD_MIN_RESTARTS) {
|
|
2160
|
+
return { pause: false, reasons: [] };
|
|
2161
|
+
}
|
|
2162
|
+
const signals = detectDaemonRestartStormSignals({ logDir });
|
|
2163
|
+
if (!signals.hasSignal) return { pause: false, reasons: [] };
|
|
2164
|
+
return { pause: true, reasons: signals.reasons };
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2083
2167
|
function detectExistingMonitorLockOwner(excludePid = null) {
|
|
2084
2168
|
try {
|
|
2085
2169
|
for (const pidFile of getMonitorPidFileCandidates()) {
|
|
@@ -2213,6 +2297,19 @@ function runMonitor({ restartReason = "" } = {}) {
|
|
|
2213
2297
|
DAEMON_MAX_RESTART_DELAY_MS,
|
|
2214
2298
|
);
|
|
2215
2299
|
const delayMs = isOSKill ? 5000 : backoffDelay;
|
|
2300
|
+
const restartStormGuard = shouldPauseDaemonRestartStorm({
|
|
2301
|
+
restartCount: daemonRestartCount,
|
|
2302
|
+
});
|
|
2303
|
+
if (restartStormGuard.pause) {
|
|
2304
|
+
const reasonLabel = restartStormGuard.reasons.join(", ");
|
|
2305
|
+
console.error(
|
|
2306
|
+
`\n :close: Monitor restart storm paused after ${daemonRestartCount} attempts due to persistent runtime issues (${reasonLabel}).`,
|
|
2307
|
+
);
|
|
2308
|
+
sendCrashNotification(exitCode, signal).finally(() =>
|
|
2309
|
+
process.exit(exitCode),
|
|
2310
|
+
);
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2216
2313
|
if (IS_DAEMON_CHILD && crashState.exceeded) {
|
|
2217
2314
|
const durationSec = Math.max(
|
|
2218
2315
|
1,
|
package/infra/monitor.mjs
CHANGED
|
@@ -197,7 +197,7 @@ import {
|
|
|
197
197
|
installConsoleInterceptor,
|
|
198
198
|
setErrorLogFile,
|
|
199
199
|
} from "../lib/logger.mjs";
|
|
200
|
-
import { fixGitConfigCorruption } from "../workspace/worktree-manager.mjs";
|
|
200
|
+
import { fixGitConfigCorruption, listActiveWorktrees } from "../workspace/worktree-manager.mjs";
|
|
201
201
|
// ── Task management subsystem imports ──────────────────────────────────────
|
|
202
202
|
import {
|
|
203
203
|
configureTaskStore,
|
|
@@ -14062,6 +14062,98 @@ safeSetInterval("workflow-review-merge-reconcile", async () => {
|
|
|
14062
14062
|
|
|
14063
14063
|
// Legacy merged PR check removed (workflow-only control).
|
|
14064
14064
|
|
|
14065
|
+
// ── Periodic stale worktree sync: every 5 min ─────────────────────────────
|
|
14066
|
+
// Detects task worktrees that are diverged from their remote (local commits
|
|
14067
|
+
// not pushed, or remote has newer commits from a previous run). For each
|
|
14068
|
+
// diverged worktree it: fetches remote, rebases local onto the remote tracking
|
|
14069
|
+
// ref for that branch, then pushes with --force-with-lease. This keeps the
|
|
14070
|
+
// VS Code source-control view clean and unblocks tasks that got stuck mid-push.
|
|
14071
|
+
async function syncDivergedWorktrees() {
|
|
14072
|
+
const worktrees = listActiveWorktrees(repoRoot);
|
|
14073
|
+
let synced = 0;
|
|
14074
|
+
let failed = 0;
|
|
14075
|
+
|
|
14076
|
+
for (const wt of worktrees) {
|
|
14077
|
+
const { path: wtPath, branch } = wt;
|
|
14078
|
+
if (!wtPath || !branch || !branch.startsWith("task/")) continue;
|
|
14079
|
+
|
|
14080
|
+
try {
|
|
14081
|
+
// Fetch remote to update tracking refs
|
|
14082
|
+
execSync("git fetch origin --no-tags", {
|
|
14083
|
+
cwd: wtPath, timeout: 30_000, stdio: ["ignore", "pipe", "pipe"],
|
|
14084
|
+
});
|
|
14085
|
+
|
|
14086
|
+
// Check ahead/behind vs remote tracking ref
|
|
14087
|
+
const remoteRef = `origin/${branch}`;
|
|
14088
|
+
let remoteExists = false;
|
|
14089
|
+
try {
|
|
14090
|
+
execSync(`git rev-parse --verify ${remoteRef}`, {
|
|
14091
|
+
cwd: wtPath, timeout: 5_000, stdio: ["ignore", "pipe", "pipe"],
|
|
14092
|
+
});
|
|
14093
|
+
remoteExists = true;
|
|
14094
|
+
} catch { /* branch not yet pushed — nothing to sync */ }
|
|
14095
|
+
|
|
14096
|
+
if (!remoteExists) continue;
|
|
14097
|
+
|
|
14098
|
+
const ahead = parseInt(
|
|
14099
|
+
execSync(`git rev-list --count ${remoteRef}..HEAD`, {
|
|
14100
|
+
cwd: wtPath, encoding: "utf8", timeout: 10_000, stdio: ["ignore", "pipe", "pipe"],
|
|
14101
|
+
}).trim(), 10);
|
|
14102
|
+
const behind = parseInt(
|
|
14103
|
+
execSync(`git rev-list --count HEAD..${remoteRef}`, {
|
|
14104
|
+
cwd: wtPath, encoding: "utf8", timeout: 10_000, stdio: ["ignore", "pipe", "pipe"],
|
|
14105
|
+
}).trim(), 10);
|
|
14106
|
+
|
|
14107
|
+
// Only act on diverged worktrees (behind > 0 AND ahead > 0)
|
|
14108
|
+
if (ahead === 0 || behind === 0) continue;
|
|
14109
|
+
|
|
14110
|
+
console.log(
|
|
14111
|
+
`[monitor:worktree-sync] ${branch} diverged: ${ahead} ahead, ${behind} behind — rebasing and pushing`,
|
|
14112
|
+
);
|
|
14113
|
+
|
|
14114
|
+
// Rebase local onto remote tracking ref to incorporate remote commits
|
|
14115
|
+
let rebased = false;
|
|
14116
|
+
try {
|
|
14117
|
+
execSync(`git rebase ${remoteRef}`, {
|
|
14118
|
+
cwd: wtPath, encoding: "utf8", timeout: 60_000, stdio: ["ignore", "pipe", "pipe"],
|
|
14119
|
+
});
|
|
14120
|
+
rebased = true;
|
|
14121
|
+
} catch (rebaseErr) {
|
|
14122
|
+
try { execSync("git rebase --abort", { cwd: wtPath, timeout: 10_000, stdio: ["ignore", "pipe", "pipe"] }); } catch { /* ok */ }
|
|
14123
|
+
console.warn(
|
|
14124
|
+
`[monitor:worktree-sync] ${branch} rebase conflict — skipping push: ${rebaseErr.message?.slice(0, 200)}`,
|
|
14125
|
+
);
|
|
14126
|
+
failed++;
|
|
14127
|
+
continue;
|
|
14128
|
+
}
|
|
14129
|
+
|
|
14130
|
+
// Push with --force-with-lease (safe: we just fetched fresh remote refs)
|
|
14131
|
+
try {
|
|
14132
|
+
execSync(`git push --force-with-lease --set-upstream origin HEAD`, {
|
|
14133
|
+
cwd: wtPath, encoding: "utf8", timeout: 30_000, stdio: ["ignore", "pipe", "pipe"],
|
|
14134
|
+
});
|
|
14135
|
+
console.log(`[monitor:worktree-sync] ${branch} sync-pushed successfully`);
|
|
14136
|
+
synced++;
|
|
14137
|
+
} catch (pushErr) {
|
|
14138
|
+
console.warn(
|
|
14139
|
+
`[monitor:worktree-sync] ${branch} push failed: ${pushErr.message?.slice(0, 200)}`,
|
|
14140
|
+
);
|
|
14141
|
+
failed++;
|
|
14142
|
+
}
|
|
14143
|
+
} catch (err) {
|
|
14144
|
+
console.warn(`[monitor:worktree-sync] ${branch} error: ${err.message?.slice(0, 200)}`);
|
|
14145
|
+
failed++;
|
|
14146
|
+
}
|
|
14147
|
+
}
|
|
14148
|
+
|
|
14149
|
+
if (synced > 0 || failed > 0) {
|
|
14150
|
+
console.log(`[monitor:worktree-sync] cycle complete — synced=${synced} failed=${failed}`);
|
|
14151
|
+
}
|
|
14152
|
+
}
|
|
14153
|
+
|
|
14154
|
+
const worktreeSyncIntervalMs = 5 * 60 * 1000; // 5 min
|
|
14155
|
+
safeSetInterval("worktree-sync", syncDivergedWorktrees, worktreeSyncIntervalMs);
|
|
14156
|
+
|
|
14065
14157
|
// ── Periodic epic branch sync/merge: every 15 min ──────────────────────────
|
|
14066
14158
|
const epicMergeIntervalMs = 15 * 60 * 1000;
|
|
14067
14159
|
safeSetInterval("epic-merge-check", () => checkEpicBranches("interval"), epicMergeIntervalMs);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.13",
|
|
4
4
|
"description": "Bosun Autonomous Engineering — manages AI agent executors with failover, extremely powerful workflow builder, and a massive amount of included default workflow templates for autonomous engineering, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
package/ui/demo-defaults.js
CHANGED
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"type": "action.run_command",
|
|
80
80
|
"label": "Fetch, Classify & Label PRs",
|
|
81
81
|
"config": {
|
|
82
|
-
"command": "node -e \" const fs=require('fs'); const path=require('path'); const {execFileSync}=require('child_process'); const LABEL_FIX='{{labelNeedsFix}}'; const MAX_PRS=Math.max(1,Number('{{maxPrs}}')||25); const REPO_SCOPE=String('{{repoScope}}'||'auto').trim(); const FIELDS='number,title,headRefName,baseRefName,isDraft,mergeable,statusCheckRollup,labels,url'; const FAIL_STATES=new Set(['FAILURE','ERROR','TIMED_OUT','CANCELLED','STARTUP_FAILURE']); const PEND_STATES=new Set(['PENDING','IN_PROGRESS','QUEUED','WAITING','REQUESTED','EXPECTED']); const CONFLICT_MERGEABLES=new Set(['CONFLICTING','BEHIND','DIRTY']); function ghJson(args){const out=execFileSync('gh',args,{encoding:'utf8',stdio:['pipe','pipe','pipe']}).trim();return out?JSON.parse(out):[];} function configPath(){ const home=String(process.env.BOSUN_HOME||process.env.VK_PROJECT_DIR||'').trim(); return home?path.join(home,'bosun.config.json'):path.join(process.cwd(),'bosun.config.json'); } function collectReposFromConfig(){ const repos=[]; try{ const cfg=JSON.parse(fs.readFileSync(configPath(),'utf8')); const workspaces=Array.isArray(cfg?.workspaces)?cfg.workspaces:[]; if(workspaces.length>0){ const active=String(cfg?.activeWorkspace||'').trim().toLowerCase(); const activeWs=active?workspaces.find(w=>String(w?.id||'').trim().toLowerCase()===active):null; const wsList=activeWs?[activeWs]:workspaces; for(const ws of wsList){ for(const repo of (Array.isArray(ws?.repos)?ws.repos:[])){ const slug=typeof repo==='string'?String(repo).trim():String(repo?.slug||'').trim(); if(slug) repos.push(slug); } } } if(repos.length===0){ for(const repo of (Array.isArray(cfg?.repos)?cfg.repos:[])){ const slug=typeof repo==='string'?String(repo).trim():String(repo?.slug||'').trim(); if(slug) repos.push(slug); } } }catch{} return repos; } function resolveRepoTargets(){ if(REPO_SCOPE&&REPO_SCOPE!=='auto'&&REPO_SCOPE!=='all'&&REPO_SCOPE!=='current'){ return [...new Set(REPO_SCOPE.split(',').map(v=>v.trim()).filter(Boolean))]; } if(REPO_SCOPE==='current') return ['']; const fromConfig=collectReposFromConfig(); if(fromConfig.length>0) return [...new Set(fromConfig)]; const envRepo=String(process.env.GITHUB_REPOSITORY||'').trim(); if(envRepo) return [envRepo]; return ['']; } function parseRepoFromUrl(url){ const raw=String(url||''); const marker='github.com/'; const idx=raw.toLowerCase().indexOf(marker); if(idx<0) return ''; const tail=raw.slice(idx+marker.length).split('/'); if(tail.length<2) return ''; const owner=String(tail[0]||'').trim(); const repo=String(tail[1]||'').trim(); return owner&&repo?(owner+'/'+repo):''; } const repoTargets=resolveRepoTargets(); const prs=[]; const repoErrors=[]; for(const target of repoTargets){ const repo=String(target||'').trim(); const args=['pr','list','--label','bosun-attached','--state','open','--json',FIELDS,'--limit',String(MAX_PRS)]; if(repo) args.push('--repo',repo); try{ const list=ghJson(args); for(const pr of (Array.isArray(list)?list:[])){ const prRepo=repo||parseRepoFromUrl(pr?.url)||String(process.env.GITHUB_REPOSITORY||'').trim(); prs.push({...pr,__repo:prRepo}); } }catch(e){ repoErrors.push({repo:repo||'current',error:String(e?.message||e)}); } } const readyCandidates=[],conflicts=[],ciFailures=[],pending=[],drafted=[]; let newlyLabeled=0; for(const pr of prs){ const labels=(pr.labels||[]).map(l=>typeof l==='string'?l:l?.name).filter(Boolean); const hasFixLabel=labels.includes(LABEL_FIX); const checks=pr.statusCheckRollup||[]; const hasFail=checks.some(c=>FAIL_STATES.has(c.conclusion||c.state||'')); const hasPend=checks.some(c=>PEND_STATES.has(c.conclusion||c.state||'')); const isConflict=CONFLICT_MERGEABLES.has(String(pr.mergeable||'').toUpperCase()); const isDraft=pr.isDraft===true; const repo=String(pr.__repo||'').trim(); if(isDraft){drafted.push({n:pr.number,repo});continue;} if(isConflict){ conflicts.push({n:pr.number,repo,branch:pr.headRefName,base:pr.baseRefName,url:pr.url}); if(!hasFixLabel){ try{const editArgs=['pr','edit',String(pr.number),'--add-label',LABEL_FIX];if(repo)editArgs.push('--repo',repo);execFileSync('gh',editArgs,{encoding:'utf8',stdio:['pipe','pipe','pipe']});newlyLabeled++;} catch(e){process.stderr.write('label err '+(repo?repo+' ':'')+'#'+pr.number+': '+(e?.message||e)+'\\\\n');} } } else if(hasFail){ ciFailures.push({n:pr.number,repo,branch:pr.headRefName,url:pr.url}); if(!hasFixLabel){ try{const editArgs=['pr','edit',String(pr.number),'--add-label',LABEL_FIX];if(repo)editArgs.push('--repo',repo);execFileSync('gh',editArgs,{encoding:'utf8',stdio:['pipe','pipe','pipe']});newlyLabeled++;} catch(e){process.stderr.write('label err '+(repo?repo+' ':'')+'#'+pr.number+': '+(e?.message||e)+'\\\\n');} } } else if(checks.length>0&&!hasFixLabel){
|
|
82
|
+
"command": "node -e \" const fs=require('fs'); const path=require('path'); const {execFileSync}=require('child_process'); const LABEL_FIX='{{labelNeedsFix}}'; const MAX_PRS=Math.max(1,Number('{{maxPrs}}')||25); const REPO_SCOPE=String('{{repoScope}}'||'auto').trim(); const FIELDS='number,title,headRefName,baseRefName,isDraft,mergeable,statusCheckRollup,labels,url'; const FAIL_STATES=new Set(['FAILURE','ERROR','TIMED_OUT','CANCELLED','STARTUP_FAILURE']); const PEND_STATES=new Set(['PENDING','IN_PROGRESS','QUEUED','WAITING','REQUESTED','EXPECTED']); const CONFLICT_MERGEABLES=new Set(['CONFLICTING','BEHIND','DIRTY']); function ghJson(args){const out=execFileSync('gh',args,{encoding:'utf8',stdio:['pipe','pipe','pipe']}).trim();return out?JSON.parse(out):[];} function configPath(){ const home=String(process.env.BOSUN_HOME||process.env.VK_PROJECT_DIR||'').trim(); return home?path.join(home,'bosun.config.json'):path.join(process.cwd(),'bosun.config.json'); } function collectReposFromConfig(){ const repos=[]; try{ const cfg=JSON.parse(fs.readFileSync(configPath(),'utf8')); const workspaces=Array.isArray(cfg?.workspaces)?cfg.workspaces:[]; if(workspaces.length>0){ const active=String(cfg?.activeWorkspace||'').trim().toLowerCase(); const activeWs=active?workspaces.find(w=>String(w?.id||'').trim().toLowerCase()===active):null; const wsList=activeWs?[activeWs]:workspaces; for(const ws of wsList){ for(const repo of (Array.isArray(ws?.repos)?ws.repos:[])){ const slug=typeof repo==='string'?String(repo).trim():String(repo?.slug||'').trim(); if(slug) repos.push(slug); } } } if(repos.length===0){ for(const repo of (Array.isArray(cfg?.repos)?cfg.repos:[])){ const slug=typeof repo==='string'?String(repo).trim():String(repo?.slug||'').trim(); if(slug) repos.push(slug); } } }catch{} return repos; } function resolveRepoTargets(){ if(REPO_SCOPE&&REPO_SCOPE!=='auto'&&REPO_SCOPE!=='all'&&REPO_SCOPE!=='current'){ return [...new Set(REPO_SCOPE.split(',').map(v=>v.trim()).filter(Boolean))]; } if(REPO_SCOPE==='current') return ['']; const fromConfig=collectReposFromConfig(); if(fromConfig.length>0) return [...new Set(fromConfig)]; const envRepo=String(process.env.GITHUB_REPOSITORY||'').trim(); if(envRepo) return [envRepo]; return ['']; } function parseRepoFromUrl(url){ const raw=String(url||''); const marker='github.com/'; const idx=raw.toLowerCase().indexOf(marker); if(idx<0) return ''; const tail=raw.slice(idx+marker.length).split('/'); if(tail.length<2) return ''; const owner=String(tail[0]||'').trim(); const repo=String(tail[1]||'').trim(); return owner&&repo?(owner+'/'+repo):''; } const repoTargets=resolveRepoTargets(); const prs=[]; const repoErrors=[]; for(const target of repoTargets){ const repo=String(target||'').trim(); const args=['pr','list','--label','bosun-attached','--state','open','--json',FIELDS,'--limit',String(MAX_PRS)]; if(repo) args.push('--repo',repo); try{ const list=ghJson(args); for(const pr of (Array.isArray(list)?list:[])){ const prRepo=repo||parseRepoFromUrl(pr?.url)||String(process.env.GITHUB_REPOSITORY||'').trim(); prs.push({...pr,__repo:prRepo}); } }catch(e){ repoErrors.push({repo:repo||'current',error:String(e?.message||e)}); } } const readyCandidates=[],conflicts=[],ciFailures=[],pending=[],drafted=[]; let newlyLabeled=0,staleLabelCleared=0,ciKicked=0; for(const pr of prs){ const labels=(pr.labels||[]).map(l=>typeof l==='string'?l:l?.name).filter(Boolean); const hasFixLabel=labels.includes(LABEL_FIX); const checks=pr.statusCheckRollup||[]; const hasFail=checks.some(c=>FAIL_STATES.has(c.conclusion||c.state||'')); const hasPend=checks.some(c=>PEND_STATES.has(c.conclusion||c.state||'')); const isConflict=CONFLICT_MERGEABLES.has(String(pr.mergeable||'').toUpperCase()); const isDraft=pr.isDraft===true; const repo=String(pr.__repo||'').trim(); if(isDraft){drafted.push({n:pr.number,repo});continue;} if(isConflict){ conflicts.push({n:pr.number,repo,branch:pr.headRefName,base:pr.baseRefName,url:pr.url}); if(!hasFixLabel){ try{const editArgs=['pr','edit',String(pr.number),'--add-label',LABEL_FIX];if(repo)editArgs.push('--repo',repo);execFileSync('gh',editArgs,{encoding:'utf8',stdio:['pipe','pipe','pipe']});newlyLabeled++;} catch(e){process.stderr.write('label err '+(repo?repo+' ':'')+'#'+pr.number+': '+(e?.message||e)+'\\\\n');} } } else if(hasFail){ ciFailures.push({n:pr.number,repo,branch:pr.headRefName,url:pr.url}); if(!hasFixLabel){ try{const editArgs=['pr','edit',String(pr.number),'--add-label',LABEL_FIX];if(repo)editArgs.push('--repo',repo);execFileSync('gh',editArgs,{encoding:'utf8',stdio:['pipe','pipe','pipe']});newlyLabeled++;} catch(e){process.stderr.write('label err '+(repo?repo+' ':'')+'#'+pr.number+': '+(e?.message||e)+'\\\\n');} } } else { if(hasFixLabel&&!hasPend){ try{ const rmArgs=['pr','edit',String(pr.number),'--remove-label',LABEL_FIX]; if(repo)rmArgs.push('--repo',repo); execFileSync('gh',rmArgs,{encoding:'utf8',stdio:['pipe','pipe','pipe']}); staleLabelCleared++; }catch(e){process.stderr.write('stale-label-rm err '+(repo?repo+' ':'')+'#'+pr.number+': '+(e?.message||e)+'\\\\n');} } else if(checks.length>0&&!hasFixLabel){ if(hasPend) pending.push({n:pr.number,repo}); readyCandidates.push({n:pr.number,repo,branch:pr.headRefName,base:pr.baseRefName,url:pr.url,title:pr.title,pendingChecks:hasPend}); } if(checks.length===0&&repo&&pr.headRefName&&!isDraft){ try{execFileSync('gh',['workflow','run','ci.yaml','--repo',repo,'--ref',pr.headRefName],{encoding:'utf8',stdio:['pipe','pipe','pipe']});ciKicked++;} catch{} } } } console.log(JSON.stringify({ total:prs.length, reposScanned:repoTargets.length, repoErrors, readyCandidates, conflicts, ciFailures, pending:pending.length, drafted:drafted.length, newlyLabeled, staleLabelCleared, ciKicked, fixNeeded:conflicts.length+ciFailures.length })); \"",
|
|
83
83
|
"continueOnError": false,
|
|
84
84
|
"failOnError": true
|
|
85
85
|
},
|
|
@@ -17000,7 +17000,7 @@
|
|
|
17000
17000
|
"type": "action.run_command",
|
|
17001
17001
|
"label": "Fetch, Classify & Label PRs",
|
|
17002
17002
|
"config": {
|
|
17003
|
-
"command": "node -e \" const fs=require('fs'); const path=require('path'); const {execFileSync}=require('child_process'); const LABEL_FIX='{{labelNeedsFix}}'; const MAX_PRS=Math.max(1,Number('{{maxPrs}}')||25); const REPO_SCOPE=String('{{repoScope}}'||'auto').trim(); const FIELDS='number,title,headRefName,baseRefName,isDraft,mergeable,statusCheckRollup,labels,url'; const FAIL_STATES=new Set(['FAILURE','ERROR','TIMED_OUT','CANCELLED','STARTUP_FAILURE']); const PEND_STATES=new Set(['PENDING','IN_PROGRESS','QUEUED','WAITING','REQUESTED','EXPECTED']); const CONFLICT_MERGEABLES=new Set(['CONFLICTING','BEHIND','DIRTY']); function ghJson(args){const out=execFileSync('gh',args,{encoding:'utf8',stdio:['pipe','pipe','pipe']}).trim();return out?JSON.parse(out):[];} function configPath(){ const home=String(process.env.BOSUN_HOME||process.env.VK_PROJECT_DIR||'').trim(); return home?path.join(home,'bosun.config.json'):path.join(process.cwd(),'bosun.config.json'); } function collectReposFromConfig(){ const repos=[]; try{ const cfg=JSON.parse(fs.readFileSync(configPath(),'utf8')); const workspaces=Array.isArray(cfg?.workspaces)?cfg.workspaces:[]; if(workspaces.length>0){ const active=String(cfg?.activeWorkspace||'').trim().toLowerCase(); const activeWs=active?workspaces.find(w=>String(w?.id||'').trim().toLowerCase()===active):null; const wsList=activeWs?[activeWs]:workspaces; for(const ws of wsList){ for(const repo of (Array.isArray(ws?.repos)?ws.repos:[])){ const slug=typeof repo==='string'?String(repo).trim():String(repo?.slug||'').trim(); if(slug) repos.push(slug); } } } if(repos.length===0){ for(const repo of (Array.isArray(cfg?.repos)?cfg.repos:[])){ const slug=typeof repo==='string'?String(repo).trim():String(repo?.slug||'').trim(); if(slug) repos.push(slug); } } }catch{} return repos; } function resolveRepoTargets(){ if(REPO_SCOPE&&REPO_SCOPE!=='auto'&&REPO_SCOPE!=='all'&&REPO_SCOPE!=='current'){ return [...new Set(REPO_SCOPE.split(',').map(v=>v.trim()).filter(Boolean))]; } if(REPO_SCOPE==='current') return ['']; const fromConfig=collectReposFromConfig(); if(fromConfig.length>0) return [...new Set(fromConfig)]; const envRepo=String(process.env.GITHUB_REPOSITORY||'').trim(); if(envRepo) return [envRepo]; return ['']; } function parseRepoFromUrl(url){ const raw=String(url||''); const marker='github.com/'; const idx=raw.toLowerCase().indexOf(marker); if(idx<0) return ''; const tail=raw.slice(idx+marker.length).split('/'); if(tail.length<2) return ''; const owner=String(tail[0]||'').trim(); const repo=String(tail[1]||'').trim(); return owner&&repo?(owner+'/'+repo):''; } const repoTargets=resolveRepoTargets(); const prs=[]; const repoErrors=[]; for(const target of repoTargets){ const repo=String(target||'').trim(); const args=['pr','list','--label','bosun-attached','--state','open','--json',FIELDS,'--limit',String(MAX_PRS)]; if(repo) args.push('--repo',repo); try{ const list=ghJson(args); for(const pr of (Array.isArray(list)?list:[])){ const prRepo=repo||parseRepoFromUrl(pr?.url)||String(process.env.GITHUB_REPOSITORY||'').trim(); prs.push({...pr,__repo:prRepo}); } }catch(e){ repoErrors.push({repo:repo||'current',error:String(e?.message||e)}); } } const readyCandidates=[],conflicts=[],ciFailures=[],pending=[],drafted=[]; let newlyLabeled=0; for(const pr of prs){ const labels=(pr.labels||[]).map(l=>typeof l==='string'?l:l?.name).filter(Boolean); const hasFixLabel=labels.includes(LABEL_FIX); const checks=pr.statusCheckRollup||[]; const hasFail=checks.some(c=>FAIL_STATES.has(c.conclusion||c.state||'')); const hasPend=checks.some(c=>PEND_STATES.has(c.conclusion||c.state||'')); const isConflict=CONFLICT_MERGEABLES.has(String(pr.mergeable||'').toUpperCase()); const isDraft=pr.isDraft===true; const repo=String(pr.__repo||'').trim(); if(isDraft){drafted.push({n:pr.number,repo});continue;} if(isConflict){ conflicts.push({n:pr.number,repo,branch:pr.headRefName,base:pr.baseRefName,url:pr.url}); if(!hasFixLabel){ try{const editArgs=['pr','edit',String(pr.number),'--add-label',LABEL_FIX];if(repo)editArgs.push('--repo',repo);execFileSync('gh',editArgs,{encoding:'utf8',stdio:['pipe','pipe','pipe']});newlyLabeled++;} catch(e){process.stderr.write('label err '+(repo?repo+' ':'')+'#'+pr.number+': '+(e?.message||e)+'\\\\n');} } } else if(hasFail){ ciFailures.push({n:pr.number,repo,branch:pr.headRefName,url:pr.url}); if(!hasFixLabel){ try{const editArgs=['pr','edit',String(pr.number),'--add-label',LABEL_FIX];if(repo)editArgs.push('--repo',repo);execFileSync('gh',editArgs,{encoding:'utf8',stdio:['pipe','pipe','pipe']});newlyLabeled++;} catch(e){process.stderr.write('label err '+(repo?repo+' ':'')+'#'+pr.number+': '+(e?.message||e)+'\\\\n');} } } else if(checks.length>0&&!hasFixLabel){
|
|
17003
|
+
"command": "node -e \" const fs=require('fs'); const path=require('path'); const {execFileSync}=require('child_process'); const LABEL_FIX='{{labelNeedsFix}}'; const MAX_PRS=Math.max(1,Number('{{maxPrs}}')||25); const REPO_SCOPE=String('{{repoScope}}'||'auto').trim(); const FIELDS='number,title,headRefName,baseRefName,isDraft,mergeable,statusCheckRollup,labels,url'; const FAIL_STATES=new Set(['FAILURE','ERROR','TIMED_OUT','CANCELLED','STARTUP_FAILURE']); const PEND_STATES=new Set(['PENDING','IN_PROGRESS','QUEUED','WAITING','REQUESTED','EXPECTED']); const CONFLICT_MERGEABLES=new Set(['CONFLICTING','BEHIND','DIRTY']); function ghJson(args){const out=execFileSync('gh',args,{encoding:'utf8',stdio:['pipe','pipe','pipe']}).trim();return out?JSON.parse(out):[];} function configPath(){ const home=String(process.env.BOSUN_HOME||process.env.VK_PROJECT_DIR||'').trim(); return home?path.join(home,'bosun.config.json'):path.join(process.cwd(),'bosun.config.json'); } function collectReposFromConfig(){ const repos=[]; try{ const cfg=JSON.parse(fs.readFileSync(configPath(),'utf8')); const workspaces=Array.isArray(cfg?.workspaces)?cfg.workspaces:[]; if(workspaces.length>0){ const active=String(cfg?.activeWorkspace||'').trim().toLowerCase(); const activeWs=active?workspaces.find(w=>String(w?.id||'').trim().toLowerCase()===active):null; const wsList=activeWs?[activeWs]:workspaces; for(const ws of wsList){ for(const repo of (Array.isArray(ws?.repos)?ws.repos:[])){ const slug=typeof repo==='string'?String(repo).trim():String(repo?.slug||'').trim(); if(slug) repos.push(slug); } } } if(repos.length===0){ for(const repo of (Array.isArray(cfg?.repos)?cfg.repos:[])){ const slug=typeof repo==='string'?String(repo).trim():String(repo?.slug||'').trim(); if(slug) repos.push(slug); } } }catch{} return repos; } function resolveRepoTargets(){ if(REPO_SCOPE&&REPO_SCOPE!=='auto'&&REPO_SCOPE!=='all'&&REPO_SCOPE!=='current'){ return [...new Set(REPO_SCOPE.split(',').map(v=>v.trim()).filter(Boolean))]; } if(REPO_SCOPE==='current') return ['']; const fromConfig=collectReposFromConfig(); if(fromConfig.length>0) return [...new Set(fromConfig)]; const envRepo=String(process.env.GITHUB_REPOSITORY||'').trim(); if(envRepo) return [envRepo]; return ['']; } function parseRepoFromUrl(url){ const raw=String(url||''); const marker='github.com/'; const idx=raw.toLowerCase().indexOf(marker); if(idx<0) return ''; const tail=raw.slice(idx+marker.length).split('/'); if(tail.length<2) return ''; const owner=String(tail[0]||'').trim(); const repo=String(tail[1]||'').trim(); return owner&&repo?(owner+'/'+repo):''; } const repoTargets=resolveRepoTargets(); const prs=[]; const repoErrors=[]; for(const target of repoTargets){ const repo=String(target||'').trim(); const args=['pr','list','--label','bosun-attached','--state','open','--json',FIELDS,'--limit',String(MAX_PRS)]; if(repo) args.push('--repo',repo); try{ const list=ghJson(args); for(const pr of (Array.isArray(list)?list:[])){ const prRepo=repo||parseRepoFromUrl(pr?.url)||String(process.env.GITHUB_REPOSITORY||'').trim(); prs.push({...pr,__repo:prRepo}); } }catch(e){ repoErrors.push({repo:repo||'current',error:String(e?.message||e)}); } } const readyCandidates=[],conflicts=[],ciFailures=[],pending=[],drafted=[]; let newlyLabeled=0,staleLabelCleared=0,ciKicked=0; for(const pr of prs){ const labels=(pr.labels||[]).map(l=>typeof l==='string'?l:l?.name).filter(Boolean); const hasFixLabel=labels.includes(LABEL_FIX); const checks=pr.statusCheckRollup||[]; const hasFail=checks.some(c=>FAIL_STATES.has(c.conclusion||c.state||'')); const hasPend=checks.some(c=>PEND_STATES.has(c.conclusion||c.state||'')); const isConflict=CONFLICT_MERGEABLES.has(String(pr.mergeable||'').toUpperCase()); const isDraft=pr.isDraft===true; const repo=String(pr.__repo||'').trim(); if(isDraft){drafted.push({n:pr.number,repo});continue;} if(isConflict){ conflicts.push({n:pr.number,repo,branch:pr.headRefName,base:pr.baseRefName,url:pr.url}); if(!hasFixLabel){ try{const editArgs=['pr','edit',String(pr.number),'--add-label',LABEL_FIX];if(repo)editArgs.push('--repo',repo);execFileSync('gh',editArgs,{encoding:'utf8',stdio:['pipe','pipe','pipe']});newlyLabeled++;} catch(e){process.stderr.write('label err '+(repo?repo+' ':'')+'#'+pr.number+': '+(e?.message||e)+'\\\\n');} } } else if(hasFail){ ciFailures.push({n:pr.number,repo,branch:pr.headRefName,url:pr.url}); if(!hasFixLabel){ try{const editArgs=['pr','edit',String(pr.number),'--add-label',LABEL_FIX];if(repo)editArgs.push('--repo',repo);execFileSync('gh',editArgs,{encoding:'utf8',stdio:['pipe','pipe','pipe']});newlyLabeled++;} catch(e){process.stderr.write('label err '+(repo?repo+' ':'')+'#'+pr.number+': '+(e?.message||e)+'\\\\n');} } } else { if(hasFixLabel&&!hasPend){ try{ const rmArgs=['pr','edit',String(pr.number),'--remove-label',LABEL_FIX]; if(repo)rmArgs.push('--repo',repo); execFileSync('gh',rmArgs,{encoding:'utf8',stdio:['pipe','pipe','pipe']}); staleLabelCleared++; }catch(e){process.stderr.write('stale-label-rm err '+(repo?repo+' ':'')+'#'+pr.number+': '+(e?.message||e)+'\\\\n');} } else if(checks.length>0&&!hasFixLabel){ if(hasPend) pending.push({n:pr.number,repo}); readyCandidates.push({n:pr.number,repo,branch:pr.headRefName,base:pr.baseRefName,url:pr.url,title:pr.title,pendingChecks:hasPend}); } if(checks.length===0&&repo&&pr.headRefName&&!isDraft){ try{execFileSync('gh',['workflow','run','ci.yaml','--repo',repo,'--ref',pr.headRefName],{encoding:'utf8',stdio:['pipe','pipe','pipe']});ciKicked++;} catch{} } } } console.log(JSON.stringify({ total:prs.length, reposScanned:repoTargets.length, repoErrors, readyCandidates, conflicts, ciFailures, pending:pending.length, drafted:drafted.length, newlyLabeled, staleLabelCleared, ciKicked, fixNeeded:conflicts.length+ciFailures.length })); \"",
|
|
17004
17004
|
"continueOnError": false,
|
|
17005
17005
|
"failOnError": true
|
|
17006
17006
|
},
|
|
@@ -28,6 +28,7 @@ import { buildRelevantSkillsPromptBlock, findRelevantSkills } from "../agent/bos
|
|
|
28
28
|
import { getSessionTracker } from "../infra/session-tracker.mjs";
|
|
29
29
|
import { fixGitConfigCorruption } from "../workspace/worktree-manager.mjs";
|
|
30
30
|
import { clearBlockedWorktreeIdentity } from "../git/git-safety.mjs";
|
|
31
|
+
import { getGitHubToken } from "../github/github-auth-manager.mjs";
|
|
31
32
|
|
|
32
33
|
const TAG = "[workflow-nodes]";
|
|
33
34
|
const PORTABLE_WORKTREE_COUNT_COMMAND = "node -e \"const cp=require('node:child_process');const wt=cp.execSync('git worktree list --porcelain',{encoding:'utf8'});const count=(wt.match(/^worktree /gm)||[]).length;process.stdout.write(String(count)+'\\\\n');\"";
|
|
@@ -3436,11 +3437,29 @@ registerNodeType("action.create_pr", {
|
|
|
3436
3437
|
BOSUN_ATTACHED_PR_LABEL,
|
|
3437
3438
|
]));
|
|
3438
3439
|
const reviewers = toList(ctx.resolve(node.config?.reviewers || ""));
|
|
3440
|
+
|
|
3441
|
+
// Resolve Bosun's best available GitHub token and inject as GH_TOKEN so that
|
|
3442
|
+
// `gh pr create` uses a user OAuth / App installation token rather than the
|
|
3443
|
+
// ambient GITHUB_TOKEN. GitHub suppresses pull_request CI workflow triggers
|
|
3444
|
+
// for events caused by GITHUB_TOKEN (loop-prevention), so using a real user
|
|
3445
|
+
// token here is what allows CI to fire automatically on the created PR.
|
|
3446
|
+
let ghTokenEnv = {};
|
|
3447
|
+
try {
|
|
3448
|
+
const [ghOwner, ghRepo] = repoSlug ? repoSlug.split("/") : [];
|
|
3449
|
+
const { token, type } = await getGitHubToken({ owner: ghOwner, repo: ghRepo });
|
|
3450
|
+
// Only inject when we have a real user/app token, not an env-fallback
|
|
3451
|
+
// (which would be GITHUB_TOKEN itself — injecting it would be redundant).
|
|
3452
|
+
if (type !== "env") {
|
|
3453
|
+
ghTokenEnv = { GH_TOKEN: token };
|
|
3454
|
+
}
|
|
3455
|
+
} catch {
|
|
3456
|
+
// No auth available — fall back to ambient environment
|
|
3457
|
+
}
|
|
3439
3458
|
const execOptions = {
|
|
3440
3459
|
cwd,
|
|
3441
3460
|
encoding: "utf8",
|
|
3442
3461
|
timeout: 60000,
|
|
3443
|
-
env: makeIsolatedGitEnv(),
|
|
3462
|
+
env: makeIsolatedGitEnv(ghTokenEnv),
|
|
3444
3463
|
stdio: ["pipe", "pipe", "pipe"],
|
|
3445
3464
|
};
|
|
3446
3465
|
|
|
@@ -9426,12 +9445,47 @@ registerNodeType("action.push_branch", {
|
|
|
9426
9445
|
return { success: false, error: `Protected branch: ${cleanBranch}`, pushed: false };
|
|
9427
9446
|
}
|
|
9428
9447
|
|
|
9448
|
+
// ── Fetch (always, independent of rebase) ──
|
|
9449
|
+
// Must succeed before push so --force-with-lease has fresh remote tracking refs.
|
|
9450
|
+
try {
|
|
9451
|
+
execSync(`git fetch ${remote} --no-tags`, {
|
|
9452
|
+
cwd: worktreePath, timeout: 30000, stdio: ["ignore", "pipe", "pipe"],
|
|
9453
|
+
});
|
|
9454
|
+
} catch (fetchErr) {
|
|
9455
|
+
ctx.log(node.id, `Fetch failed (will push anyway): ${fetchErr.message?.slice(0, 200)}`);
|
|
9456
|
+
}
|
|
9457
|
+
|
|
9429
9458
|
// ── Rebase-before-push ──
|
|
9430
9459
|
if (rebaseBeforePush) {
|
|
9460
|
+
// Step 1: if the remote already has commits on this branch (previous run / partial push),
|
|
9461
|
+
// rebase local onto origin/${cleanBranch} first so we incorporate those commits and
|
|
9462
|
+
// the subsequent push is a clean fast-forward instead of a diverged force-push.
|
|
9463
|
+
const remoteTrackingRef = `${remote}/${cleanBranch}`;
|
|
9431
9464
|
try {
|
|
9432
|
-
execSync(`git
|
|
9433
|
-
cwd: worktreePath, timeout:
|
|
9465
|
+
execSync(`git rev-parse --verify ${remoteTrackingRef}`, {
|
|
9466
|
+
cwd: worktreePath, timeout: 5000, stdio: ["ignore", "pipe", "pipe"],
|
|
9434
9467
|
});
|
|
9468
|
+
// Remote branch exists — check if it diverges from local
|
|
9469
|
+
const behindCount = execSync(
|
|
9470
|
+
`git rev-list --count HEAD..${remoteTrackingRef}`,
|
|
9471
|
+
{ cwd: worktreePath, encoding: "utf8", timeout: 10000, stdio: ["ignore", "pipe", "pipe"] }
|
|
9472
|
+
).trim();
|
|
9473
|
+
if (parseInt(behindCount, 10) > 0) {
|
|
9474
|
+
try {
|
|
9475
|
+
execSync(`git rebase ${remoteTrackingRef}`, {
|
|
9476
|
+
cwd: worktreePath, encoding: "utf8", timeout: 60000,
|
|
9477
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
9478
|
+
});
|
|
9479
|
+
ctx.log(node.id, `Synced local with ${remoteTrackingRef} (was ${behindCount} behind)`);
|
|
9480
|
+
} catch (syncErr) {
|
|
9481
|
+
try { execSync("git rebase --abort", { cwd: worktreePath, timeout: 10000, stdio: ["ignore", "pipe", "pipe"] }); } catch { /* ok */ }
|
|
9482
|
+
ctx.log(node.id, `Sync with ${remoteTrackingRef} conflicted, skipping: ${syncErr.message?.slice(0, 200)}`);
|
|
9483
|
+
}
|
|
9484
|
+
}
|
|
9485
|
+
} catch { /* remote branch doesn't exist yet — normal for first push */ }
|
|
9486
|
+
|
|
9487
|
+
// Step 2: rebase onto base branch (e.g. origin/main)
|
|
9488
|
+
try {
|
|
9435
9489
|
execSync(`git rebase ${baseBranch}`, {
|
|
9436
9490
|
cwd: worktreePath, encoding: "utf8", timeout: 60000,
|
|
9437
9491
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -9444,7 +9498,7 @@ registerNodeType("action.push_branch", {
|
|
|
9444
9498
|
cwd: worktreePath, timeout: 10000, stdio: ["ignore", "pipe", "pipe"],
|
|
9445
9499
|
});
|
|
9446
9500
|
} catch { /* already aborted */ }
|
|
9447
|
-
ctx.log(node.id, `Rebase
|
|
9501
|
+
ctx.log(node.id, `Rebase onto ${baseBranch} conflicted, skipping: ${rebaseErr.message?.slice(0, 200)}`);
|
|
9448
9502
|
}
|
|
9449
9503
|
}
|
|
9450
9504
|
|
|
@@ -830,7 +830,7 @@ export const BOSUN_PR_WATCHDOG_TEMPLATE = {
|
|
|
830
830
|
" }",
|
|
831
831
|
"}",
|
|
832
832
|
"const readyCandidates=[],conflicts=[],ciFailures=[],pending=[],drafted=[];",
|
|
833
|
-
"let newlyLabeled=0;",
|
|
833
|
+
"let newlyLabeled=0,staleLabelCleared=0,ciKicked=0;",
|
|
834
834
|
"for(const pr of prs){",
|
|
835
835
|
" const labels=(pr.labels||[]).map(l=>typeof l==='string'?l:l?.name).filter(Boolean);",
|
|
836
836
|
" const hasFixLabel=labels.includes(LABEL_FIX);",
|
|
@@ -853,9 +853,22 @@ export const BOSUN_PR_WATCHDOG_TEMPLATE = {
|
|
|
853
853
|
" try{const editArgs=['pr','edit',String(pr.number),'--add-label',LABEL_FIX];if(repo)editArgs.push('--repo',repo);execFileSync('gh',editArgs,{encoding:'utf8',stdio:['pipe','pipe','pipe']});newlyLabeled++;}",
|
|
854
854
|
" catch(e){process.stderr.write('label err '+(repo?repo+' ':'')+'#'+pr.number+': '+(e?.message||e)+'\\\\n');}",
|
|
855
855
|
" }",
|
|
856
|
-
" } else
|
|
857
|
-
" if(hasPend)
|
|
858
|
-
"
|
|
856
|
+
" } else {",
|
|
857
|
+
" if(hasFixLabel&&!hasPend){",
|
|
858
|
+
" try{",
|
|
859
|
+
" const rmArgs=['pr','edit',String(pr.number),'--remove-label',LABEL_FIX];",
|
|
860
|
+
" if(repo)rmArgs.push('--repo',repo);",
|
|
861
|
+
" execFileSync('gh',rmArgs,{encoding:'utf8',stdio:['pipe','pipe','pipe']});",
|
|
862
|
+
" staleLabelCleared++;",
|
|
863
|
+
" }catch(e){process.stderr.write('stale-label-rm err '+(repo?repo+' ':'')+'#'+pr.number+': '+(e?.message||e)+'\\\\n');}",
|
|
864
|
+
" } else if(checks.length>0&&!hasFixLabel){",
|
|
865
|
+
" if(hasPend) pending.push({n:pr.number,repo});",
|
|
866
|
+
" readyCandidates.push({n:pr.number,repo,branch:pr.headRefName,base:pr.baseRefName,url:pr.url,title:pr.title,pendingChecks:hasPend});",
|
|
867
|
+
" }",
|
|
868
|
+
" if(checks.length===0&&repo&&pr.headRefName&&!isDraft){",
|
|
869
|
+
" try{execFileSync('gh',['workflow','run','ci.yaml','--repo',repo,'--ref',pr.headRefName],{encoding:'utf8',stdio:['pipe','pipe','pipe']});ciKicked++;}",
|
|
870
|
+
" catch{}",
|
|
871
|
+
" }",
|
|
859
872
|
" }",
|
|
860
873
|
"}",
|
|
861
874
|
"console.log(JSON.stringify({",
|
|
@@ -868,6 +881,8 @@ export const BOSUN_PR_WATCHDOG_TEMPLATE = {
|
|
|
868
881
|
" pending:pending.length,",
|
|
869
882
|
" drafted:drafted.length,",
|
|
870
883
|
" newlyLabeled,",
|
|
884
|
+
" staleLabelCleared,",
|
|
885
|
+
" ciKicked,",
|
|
871
886
|
" fixNeeded:conflicts.length+ciFailures.length",
|
|
872
887
|
"}));",
|
|
873
888
|
"\"",
|