repo-rule-detector: summarize rulesets PR parameters

This commit is contained in:
2026-03-08 09:14:55 +10:00
parent 9de357f7a9
commit dc79cec2e3
2 changed files with 130 additions and 7 deletions
+65
View File
@@ -67,6 +67,71 @@ clawkeeper restore-apply ~/openclaw-backup.tgz
clawkeeper restore-apply ~/openclaw-backup.tgz --restart-gateway 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 ## Safety notes
This tool edits `~/.openclaw/openclaw.json`. This tool edits `~/.openclaw/openclaw.json`.
- Always makes a timestamped backup before modifying config. - Always makes a timestamped backup before modifying config.
+62 -4
View File
@@ -20,7 +20,18 @@
import { execFileSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
function sh(args) { 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) { function parseArgs(argv) {
@@ -57,18 +68,58 @@ function main() {
rulesets = []; 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 const relevantRulesets = rulesets
.filter(rs => rs?.enforcement && rs.enforcement !== 'disabled') .filter(rs => rs?.enforcement && rs.enforcement !== 'disabled')
.filter(rs => { .filter(rs => {
const targets = rs?.conditions?.ref_name?.include ?? []; const targets = rs?.conditions?.ref_name?.include ?? [];
return targets.length === 0 || targets.includes('~DEFAULT_BRANCH') || targets.includes(branch) || targets.includes(`refs/heads/${branch}`); return targets.length === 0 || targets.includes('~DEFAULT_BRANCH') || targets.includes(branch) || targets.includes(`refs/heads/${branch}`);
}) })
.map(rs => ({ .map(rs => {
const details = fetchRulesetDetails(rs.id);
const rulesSummary = summarizeRulesetRules(details?.rules ?? []);
return {
id: rs.id, id: rs.id,
name: rs.name, name: rs.name,
enforcement: rs.enforcement, enforcement: rs.enforcement,
target: rs?.conditions?.ref_name ?? null target: rs?.conditions?.ref_name ?? null,
})); rules: rulesSummary
};
});
const allowedMergeMethods = { const allowedMergeMethods = {
mergeCommit: !!repoInfo.allow_merge_commit, mergeCommit: !!repoInfo.allow_merge_commit,
@@ -76,12 +127,19 @@ function main() {
rebase: !!repoInfo.allow_rebase_merge 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 = { const out = {
repo: repoInfo.full_name, repo: repoInfo.full_name,
url: repoInfo.html_url, url: repoInfo.html_url,
defaultBranch: repoInfo.default_branch, defaultBranch: repoInfo.default_branch,
branch, branch,
allowedMergeMethods, allowedMergeMethods,
rulesetsRulePresence,
branchProtection: bp ? { branchProtection: bp ? {
enabled: true, enabled: true,
requiredStatusChecks: bp.required_status_checks ? { requiredStatusChecks: bp.required_status_checks ? {