diff --git a/README.md b/README.md index 5a40131..b0f7b18 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,71 @@ clawkeeper restore-apply ~/openclaw-backup.tgz clawkeeper restore-apply ~/openclaw-backup.tgz --restart-gateway ``` +## Repo rule detector (script) +If you need to quickly see what GitHub repo rules might block merges (repo rulesets + classic branch protection), run: + +```bash +node scripts/repo-rule-detector.mjs dtzp555-max/ocm --branch main +``` + +Example output (dtzp555-max/ocm): + +```json +{ + "repo": "dtzp555-max/ocm", + "url": "https://github.com/dtzp555-max/ocm", + "defaultBranch": "main", + "branch": "main", + "allowedMergeMethods": { + "mergeCommit": true, + "squash": true, + "rebase": true + }, + "rulesetsRulePresence": { + "required_linear_history": true, + "non_fast_forward": true, + "deletion": true + }, + "branchProtection": { + "enabled": false + }, + "rulesets": [ + { + "id": 13362557, + "name": "ocm", + "enforcement": "active", + "target": null, + "rules": { + "pull_request": { + "present": true, + "parameters": { + "required_review_thread_resolution": true, + "allowed_merge_methods": [ + "merge", + "squash", + "rebase" + ], + "required_approving_review_count": 0, + "require_code_owner_review": false, + "dismiss_stale_reviews_on_push": false, + "require_last_push_approval": false + } + }, + "required_linear_history": { + "present": true + }, + "non_fast_forward": { + "present": true + }, + "deletion": { + "present": true + } + } + } + ] +} +``` + ## Safety notes This tool edits `~/.openclaw/openclaw.json`. - Always makes a timestamped backup before modifying config. diff --git a/scripts/repo-rule-detector.mjs b/scripts/repo-rule-detector.mjs index 113ba0a..870be20 100755 --- a/scripts/repo-rule-detector.mjs +++ b/scripts/repo-rule-detector.mjs @@ -20,7 +20,18 @@ import { execFileSync } from 'node:child_process'; function sh(args) { - return execFileSync('gh', args, { encoding: 'utf8' }).trim(); + // Keep stdout machine-readable: never let gh chatter leak into our output. + return execFileSync('gh', args, { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'] + }).trim(); +} + +function pick(obj, keys) { + if (!obj) return null; + const out = {}; + for (const k of keys) out[k] = obj?.[k] ?? null; + return out; } function parseArgs(argv) { @@ -57,18 +68,58 @@ function main() { rulesets = []; } + const PULL_REQUEST_PARAM_KEYS = [ + 'required_review_thread_resolution', + 'allowed_merge_methods', + 'required_approving_review_count', + 'require_code_owner_review', + 'dismiss_stale_reviews_on_push', + 'require_last_push_approval' + ]; + + function fetchRulesetDetails(rulesetId) { + try { + return JSON.parse(sh(['api', `repos/${repo}/rulesets/${rulesetId}`])); + } catch { + return null; + } + } + + function hasRuleType(rules, type) { + return Array.isArray(rules) && rules.some(r => r?.type === type); + } + + function summarizeRulesetRules(rules) { + const prRule = Array.isArray(rules) ? rules.find(r => r?.type === 'pull_request') : null; + const prParams = prRule?.parameters ? pick(prRule.parameters, PULL_REQUEST_PARAM_KEYS) : null; + + return { + pull_request: prParams + ? { present: true, parameters: prParams } + : { present: false, parameters: null }, + required_linear_history: { present: hasRuleType(rules, 'required_linear_history') }, + non_fast_forward: { present: hasRuleType(rules, 'non_fast_forward') }, + deletion: { present: hasRuleType(rules, 'deletion') } + }; + } + const relevantRulesets = rulesets .filter(rs => rs?.enforcement && rs.enforcement !== 'disabled') .filter(rs => { const targets = rs?.conditions?.ref_name?.include ?? []; return targets.length === 0 || targets.includes('~DEFAULT_BRANCH') || targets.includes(branch) || targets.includes(`refs/heads/${branch}`); }) - .map(rs => ({ - id: rs.id, - name: rs.name, - enforcement: rs.enforcement, - target: rs?.conditions?.ref_name ?? null - })); + .map(rs => { + const details = fetchRulesetDetails(rs.id); + const rulesSummary = summarizeRulesetRules(details?.rules ?? []); + return { + id: rs.id, + name: rs.name, + enforcement: rs.enforcement, + target: rs?.conditions?.ref_name ?? null, + rules: rulesSummary + }; + }); const allowedMergeMethods = { mergeCommit: !!repoInfo.allow_merge_commit, @@ -76,12 +127,19 @@ function main() { rebase: !!repoInfo.allow_rebase_merge }; + const rulesetsRulePresence = { + required_linear_history: relevantRulesets.some(rs => rs?.rules?.required_linear_history?.present), + non_fast_forward: relevantRulesets.some(rs => rs?.rules?.non_fast_forward?.present), + deletion: relevantRulesets.some(rs => rs?.rules?.deletion?.present) + }; + const out = { repo: repoInfo.full_name, url: repoInfo.html_url, defaultBranch: repoInfo.default_branch, branch, allowedMergeMethods, + rulesetsRulePresence, branchProtection: bp ? { enabled: true, requiredStatusChecks: bp.required_status_checks ? {