@@ -15,6 +15,51 @@ import { detectDrift } from './detect-drift.js';
1515const GITHUB_API_BASE = 'https://api.github.com' ;
1616const ORG_NAME = 'worlddriven' ;
1717
18+ /**
19+ * Standard repository settings applied to all worlddriven repositories
20+ * Enforces: squash-only merges, branch cleanup, democratic workflow
21+ */
22+ const STANDARD_REPO_SETTINGS = {
23+ allow_squash_merge : true ,
24+ allow_merge_commit : false ,
25+ allow_rebase_merge : false ,
26+ allow_auto_merge : false ,
27+ delete_branch_on_merge : true ,
28+ allow_update_branch : false ,
29+ } ;
30+
31+ /**
32+ * Standard branch protection ruleset applied to default branch
33+ * Enforces: PR requirement, no force push, no deletion
34+ */
35+ const STANDARD_BRANCH_RULESET = {
36+ name : 'Worlddriven Democratic Governance' ,
37+ target : 'branch' ,
38+ enforcement : 'active' ,
39+ conditions : {
40+ ref_name : {
41+ include : [ '~DEFAULT_BRANCH' ] ,
42+ exclude : [ ] ,
43+ } ,
44+ } ,
45+ rules : [
46+ { type : 'deletion' } ,
47+ { type : 'non_fast_forward' } ,
48+ {
49+ type : 'pull_request' ,
50+ parameters : {
51+ required_approving_review_count : 0 ,
52+ dismiss_stale_reviews_on_push : false ,
53+ require_code_owner_review : false ,
54+ require_last_push_approval : false ,
55+ required_review_thread_resolution : false ,
56+ allowed_merge_methods : [ 'squash' ] ,
57+ } ,
58+ } ,
59+ ] ,
60+ bypass_actors : [ ] ,
61+ } ;
62+
1863/**
1964 * Create a repository in the GitHub organization
2065 */
@@ -25,9 +70,12 @@ async function createRepository(token, repoData) {
2570 name : repoData . name ,
2671 description : repoData . description ,
2772 private : false ,
73+ auto_init : true , // Creates initial README and main branch
2874 has_issues : true ,
2975 has_projects : true ,
3076 has_wiki : true ,
77+ // Apply standard settings at creation
78+ ...STANDARD_REPO_SETTINGS ,
3179 } ;
3280
3381 const response = await fetch ( url , {
@@ -49,6 +97,90 @@ async function createRepository(token, repoData) {
4997 return await response . json ( ) ;
5098}
5199
100+ /**
101+ * Create branch protection ruleset for a repository
102+ */
103+ async function createBranchProtectionRuleset ( token , repoName ) {
104+ const url = `${ GITHUB_API_BASE } /repos/${ ORG_NAME } /${ repoName } /rulesets` ;
105+
106+ const response = await fetch ( url , {
107+ method : 'POST' ,
108+ headers : {
109+ Authorization : `Bearer ${ token } ` ,
110+ Accept : 'application/vnd.github+json' ,
111+ 'X-GitHub-Api-Version' : '2022-11-28' ,
112+ 'Content-Type' : 'application/json' ,
113+ } ,
114+ body : JSON . stringify ( STANDARD_BRANCH_RULESET ) ,
115+ } ) ;
116+
117+ if ( ! response . ok ) {
118+ const error = await response . text ( ) ;
119+ throw new Error ( `GitHub API error (${ response . status } ): ${ error } ` ) ;
120+ }
121+
122+ return await response . json ( ) ;
123+ }
124+
125+ /**
126+ * Update repository settings to match standard configuration
127+ */
128+ async function updateRepositorySettings ( token , repoName ) {
129+ const url = `${ GITHUB_API_BASE } /repos/${ ORG_NAME } /${ repoName } ` ;
130+
131+ const response = await fetch ( url , {
132+ method : 'PATCH' ,
133+ headers : {
134+ Authorization : `Bearer ${ token } ` ,
135+ Accept : 'application/vnd.github+json' ,
136+ 'X-GitHub-Api-Version' : '2022-11-28' ,
137+ 'Content-Type' : 'application/json' ,
138+ } ,
139+ body : JSON . stringify ( STANDARD_REPO_SETTINGS ) ,
140+ } ) ;
141+
142+ if ( ! response . ok ) {
143+ const error = await response . text ( ) ;
144+ throw new Error ( `GitHub API error (${ response . status } ): ${ error } ` ) ;
145+ }
146+
147+ return await response . json ( ) ;
148+ }
149+
150+ /**
151+ * Ensure repository has standard configuration
152+ * Updates settings and creates ruleset if missing
153+ */
154+ async function ensureStandardConfiguration ( token , repoName ) {
155+ // Update repository settings to match standard
156+ await updateRepositorySettings ( token , repoName ) ;
157+
158+ // Check if ruleset exists
159+ const rulesetsUrl = `${ GITHUB_API_BASE } /repos/${ ORG_NAME } /${ repoName } /rulesets` ;
160+ const rulesetsResponse = await fetch ( rulesetsUrl , {
161+ method : 'GET' ,
162+ headers : {
163+ Authorization : `Bearer ${ token } ` ,
164+ Accept : 'application/vnd.github+json' ,
165+ 'X-GitHub-Api-Version' : '2022-11-28' ,
166+ } ,
167+ } ) ;
168+
169+ if ( ! rulesetsResponse . ok ) {
170+ throw new Error ( `Failed to fetch rulesets: ${ rulesetsResponse . status } ` ) ;
171+ }
172+
173+ const rulesets = await rulesetsResponse . json ( ) ;
174+ const existingRuleset = rulesets . find (
175+ ( r ) => r . name === STANDARD_BRANCH_RULESET . name
176+ ) ;
177+
178+ if ( ! existingRuleset ) {
179+ // Create new ruleset
180+ await createBranchProtectionRuleset ( token , repoName ) ;
181+ }
182+ }
183+
52184/**
53185 * Update repository description
54186 */
@@ -232,7 +364,7 @@ async function addInitializeActions(token, plan, actualRepos, desiredRepos) {
232364/**
233365 * Generate sync plan from drift
234366 */
235- function generateSyncPlan ( drift ) {
367+ function generateSyncPlan ( drift , desiredRepos ) {
236368 // Protected repositories that should never be deleted
237369 const PROTECTED_REPOS = [ 'documentation' , 'core' , 'webapp' ] ;
238370
@@ -243,6 +375,7 @@ function generateSyncPlan(drift) {
243375 updateDescription : 0 ,
244376 updateTopics : 0 ,
245377 initialize : 0 ,
378+ ensureSettings : 0 ,
246379 delete : 0 ,
247380 skip : 0 ,
248381 } ,
@@ -280,6 +413,19 @@ function generateSyncPlan(drift) {
280413 plan . summary . updateTopics ++ ;
281414 }
282415
416+ // Ensure standard settings for all existing repos (not being created or deleted)
417+ for ( const repo of desiredRepos ) {
418+ // Skip repos being created (they get settings during creation)
419+ const isBeingCreated = drift . missing . some ( ( r ) => r . name === repo . name ) ;
420+ if ( ! isBeingCreated ) {
421+ plan . actions . push ( {
422+ type : 'ensure-settings' ,
423+ repo : repo . name ,
424+ } ) ;
425+ plan . summary . ensureSettings ++ ;
426+ }
427+ }
428+
283429 // Delete extra repos (unless protected)
284430 for ( const repo of drift . extra ) {
285431 if ( PROTECTED_REPOS . includes ( repo . name ) ) {
@@ -335,14 +481,18 @@ async function executeSyncPlan(token, plan, dryRun) {
335481 switch ( action . type ) {
336482 case 'create' :
337483 result = await createRepository ( token , action . data ) ;
338- // Create initial commit so repository can be forked
339- await createInitialCommit ( token , action . data . name , action . data . description ) ;
484+ // Apply branch protection after creation (main branch now exists via auto_init)
485+ await createBranchProtectionRuleset ( token , action . data . name ) ;
340486 // After creating, set topics if they exist
341487 if ( action . data . topics && action . data . topics . length > 0 ) {
342488 await updateRepositoryTopics ( token , action . data . name , action . data . topics ) ;
343489 }
344490 break ;
345491
492+ case 'ensure-settings' :
493+ result = await ensureStandardConfiguration ( token , action . repo ) ;
494+ break ;
495+
346496 case 'initialize' :
347497 result = await createInitialCommit ( token , action . repo , action . description ) ;
348498 break ;
@@ -399,6 +549,7 @@ function formatSyncReport(plan, results, dryRun) {
399549 lines . push ( `- Update descriptions: ${ plan . summary . updateDescription } ` ) ;
400550 lines . push ( `- Update topics: ${ plan . summary . updateTopics } ` ) ;
401551 lines . push ( `- Initialize (add first commit): ${ plan . summary . initialize } ` ) ;
552+ lines . push ( `- Ensure settings: ${ plan . summary . ensureSettings } ` ) ;
402553 lines . push ( `- Delete: ${ plan . summary . delete } ` ) ;
403554 lines . push ( `- Skip (protected): ${ plan . summary . skip } ` ) ;
404555 lines . push ( '' ) ;
@@ -436,6 +587,11 @@ function formatSyncReport(plan, results, dryRun) {
436587 lines . push ( ` - Description: ${ action . description } ` ) ;
437588 break ;
438589
590+ case 'ensure-settings' :
591+ lines . push ( `- **Ensure settings** for \`${ action . repo } \`` ) ;
592+ lines . push ( ` - Applied standard configuration` ) ;
593+ break ;
594+
439595 case 'delete' :
440596 lines . push ( `- **Delete** \`${ action . repo } \`` ) ;
441597 if ( action . data . description ) {
@@ -509,7 +665,7 @@ async function main() {
509665
510666 // Generate sync plan
511667 console . error ( '📋 Generating sync plan...' ) ;
512- const plan = generateSyncPlan ( drift ) ;
668+ const plan = generateSyncPlan ( drift , desiredRepos ) ;
513669
514670 // Check for empty repositories and add initialize actions
515671 console . error ( '🔍 Checking for empty repositories...' ) ;
0 commit comments