@leverege/build-tools 2.117.0 → 2.118.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leverege/build-tools",
3
- "version": "2.117.0",
3
+ "version": "2.118.0",
4
4
  "description": "A collection of build / support tools for Leverege developers",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -19,6 +19,7 @@
19
19
  "test-all": "mocha -t 60000 --exit 'test/**/**.mjs'"
20
20
  },
21
21
  "bin": {
22
+ "branch-protect-it": "src/branch-protect-it.mjs",
22
23
  "build-cnpg-image": "src/build-cnpg-image.mjs",
23
24
  "build-tools": "src/build-tools.mjs",
24
25
  "chart-compass": "src/chart-compass.mjs",
@@ -103,7 +104,7 @@
103
104
  "googleapis": "^176.0.0",
104
105
  "handlebars": "^4.7.9",
105
106
  "ignore": "^7.0.6",
106
- "inquirer": "^14.1.0",
107
+ "inquirer": "^14.2.0",
107
108
  "js-yaml": "^4.2.0",
108
109
  "jsdoc": "^4.0.5",
109
110
  "ms": "^2.1.3",
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+ import { $ } from 'zx'
3
+ import { program } from 'commander'
4
+ import chalk from 'chalk'
5
+
6
+ import { enableDebug, errorExit, log, proceed } from './Utils.mjs'
7
+
8
+ $.verbose = false
9
+
10
+ const DEFAULT_ORG = 'Leverege'
11
+ const REQUIRED_CHECK_PREFIX = 'node-ci'
12
+
13
+ program
14
+ .name( 'branch-protect-it' )
15
+ .description( chalk.green( 'Applies the standard branch-protection + merge-method bundle to a repo' ) )
16
+ .argument( '<repo>', 'Repository name (without org prefix)' )
17
+ .option( '--org <org>', 'GitHub org', DEFAULT_ORG )
18
+ .option( '--branch <branch>', "Branch to protect (defaults to the repo's actual default branch)" )
19
+ .option( '--require-approvals <n>', 'Required approving review count', '0' )
20
+ .option( '--dry-run', 'Show what would be applied without making changes' )
21
+ .option( '--check', 'Read-only: exits 0 (matches the standard bundle) or 1 (does not) — ' +
22
+ 'no prompt, no writes, no output at all by default. For scripting/dashboards (e.g. ' +
23
+ 'repo-panopticon) that only care about the exit code. Pair with --check-verbose for output.' )
24
+ .option( '--check-verbose', 'With --check, also print which specific settings are missing/wrong ' +
25
+ '(or a success line). Has no effect without --check.' )
26
+ .option( '--debug', 'Enable debug logging' )
27
+ .addHelpText( 'after', `
28
+ ${chalk.yellow( 'What this applies:' )}
29
+ - Require a pull request before merging (0+ approvals, configurable)
30
+ - Require the repo's node-ci status check to pass, and the branch to be up to date
31
+ - Block force pushes and branch deletion
32
+ - Admins can still bypass (enforce_admins: false) for emergencies
33
+ - Repo-level merge method restricted to squash-only
34
+ - Auto-delete head branches on merge (delete_branch_on_merge)
35
+ - Suggest updating out-of-date PR branches with one click (allow_update_branch)
36
+
37
+ ${chalk.yellow( 'Scope:' )}
38
+ Only supports repos on the ${chalk.green( 'node-ci' )} reusable workflow for now — it looks up
39
+ the real check-run name from the target branch's latest commit (never guesses one) and
40
+ errors out if nothing starting with "node-ci" is found there.
41
+
42
+ ${chalk.yellow( 'Examples:' )}
43
+ ${chalk.green( 'branch-protect-it secrets' )}
44
+ ${chalk.green( 'branch-protect-it secrets --dry-run' )}
45
+ ${chalk.green( 'branch-protect-it secrets --require-approvals 1' )}
46
+ ${chalk.green( 'branch-protect-it secrets --check && echo protected' )}
47
+ ${chalk.green( 'branch-protect-it secrets --check --check-verbose' )}
48
+ ` )
49
+ .parse()
50
+
51
+ const [ repo ] = program.args
52
+ const opts = program.opts()
53
+ if ( opts.debug ) { enableDebug(); $.verbose = true }
54
+
55
+ const fullRepo = `${opts.org}/${repo}`
56
+ // --check is silent by default (exit code only) — only chatty with --check-verbose.
57
+ // Every other mode (apply/dry-run) always logs normally.
58
+ const quietCheck = opts.check && !opts.checkVerbose
59
+ const checkLog = ( ...args ) => { if ( !quietCheck ) { log( ...args ) } }
60
+
61
+ // ---------- Resolve branch ----------
62
+
63
+ let branch = opts.branch
64
+ if ( !branch ) {
65
+ const { stdout } = await $`gh api -X GET repos/${fullRepo} -q .default_branch`
66
+ branch = stdout.trim()
67
+ }
68
+ checkLog( chalk.blue( `Target: ${fullRepo}@${branch}` ) )
69
+
70
+ // ---------- --check: read-only status, exit 0/1, no writes, no prompt ----------
71
+
72
+ if ( opts.check ) {
73
+ const reasons = []
74
+
75
+ let protection = null
76
+ try {
77
+ // .quiet() suppresses gh's own stderr (e.g. "Branch not protected") — that's an
78
+ // expected, handled outcome here, not something that should leak into --check's output
79
+ const { stdout } = await $`gh api -X GET repos/${fullRepo}/branches/${branch}/protection`.quiet()
80
+ protection = JSON.parse( stdout )
81
+ } catch {
82
+ reasons.push( 'no branch protection configured at all' )
83
+ }
84
+
85
+ if ( protection ) {
86
+ if ( !protection.required_pull_request_reviews ) { reasons.push( 'PR not required' ) }
87
+ if ( !protection.required_status_checks?.strict ) { reasons.push( 'status checks not required / not strict' ) }
88
+ if ( !protection.required_status_checks?.contexts?.some( c => c.startsWith( REQUIRED_CHECK_PREFIX ) ) ) {
89
+ reasons.push( `no required check starting with "${REQUIRED_CHECK_PREFIX}"` )
90
+ }
91
+ if ( protection.allow_force_pushes?.enabled !== false ) { reasons.push( 'force pushes not blocked' ) }
92
+ if ( protection.allow_deletions?.enabled !== false ) { reasons.push( 'branch deletion not blocked' ) }
93
+ if ( protection.enforce_admins?.enabled !== false ) { reasons.push( 'enforce_admins is not false (expected: admins can bypass)' ) }
94
+ }
95
+
96
+ let repoSettings = null
97
+ try {
98
+ const { stdout } = await $`gh api -X GET repos/${fullRepo} -q '{allow_squash_merge, allow_merge_commit, allow_rebase_merge, delete_branch_on_merge, allow_update_branch}'`
99
+ repoSettings = JSON.parse( stdout )
100
+ } catch ( error ) {
101
+ reasons.push( `could not read repo merge settings: ${error.stderr ?? error.message ?? error}` )
102
+ }
103
+
104
+ if ( repoSettings ) {
105
+ if ( repoSettings.allow_squash_merge !== true ) { reasons.push( 'squash merge not enabled' ) }
106
+ if ( repoSettings.allow_merge_commit !== false ) { reasons.push( 'merge commits not disabled' ) }
107
+ if ( repoSettings.allow_rebase_merge !== false ) { reasons.push( 'rebase merge not disabled' ) }
108
+ if ( repoSettings.delete_branch_on_merge !== true ) { reasons.push( 'delete_branch_on_merge not enabled' ) }
109
+ if ( repoSettings.allow_update_branch !== true ) { reasons.push( 'allow_update_branch not enabled' ) }
110
+ }
111
+
112
+ if ( reasons.length === 0 ) {
113
+ checkLog( chalk.green( `✔ ${fullRepo}@${branch} matches the standard branch-protection bundle` ) )
114
+ process.exit( 0 )
115
+ }
116
+
117
+ checkLog( chalk.red( `✘ ${fullRepo}@${branch} does not match the standard bundle:` ) )
118
+ reasons.forEach( r => checkLog( chalk.red( ` - ${r}` ) ) )
119
+ process.exit( 1 )
120
+ }
121
+
122
+ // ---------- Find the real node-ci check name — never guess it ----------
123
+
124
+ let checkName
125
+ try {
126
+ const { stdout : shaRaw } = await $`gh api -X GET repos/${fullRepo}/commits/${branch} -q .sha`
127
+ const sha = shaRaw.trim()
128
+ const { stdout : checksRaw } = await $`gh api -X GET repos/${fullRepo}/commits/${sha}/check-runs -q '.check_runs[].name'`
129
+ const checks = checksRaw.trim().split( '\n' ).filter( Boolean )
130
+ checkName = checks.find( c => c.startsWith( REQUIRED_CHECK_PREFIX ) )
131
+ } catch ( error ) {
132
+ errorExit( `Failed to look up check runs: ${error.stderr ?? error.message ?? error}` )
133
+ }
134
+
135
+ if ( !checkName ) {
136
+ errorExit(
137
+ `No check run starting with "${REQUIRED_CHECK_PREFIX}" found on ${fullRepo}@${branch}'s latest commit.\n` +
138
+ ' Has the node-ci workflow actually run on this branch yet? Push something first, ' +
139
+ 'or pass --branch if development is not the right target.'
140
+ )
141
+ }
142
+ log( chalk.green( `Found check: ${checkName}` ) )
143
+
144
+ // ---------- Build the payload ----------
145
+
146
+ const protectionPayload = {
147
+ required_status_checks : { strict : true, contexts : [ checkName ] },
148
+ enforce_admins : false,
149
+ required_pull_request_reviews : { required_approving_review_count : Number( opts.requireApprovals ) },
150
+ restrictions : null,
151
+ allow_force_pushes : false,
152
+ allow_deletions : false,
153
+ }
154
+
155
+ log( chalk.yellow( '\nWill apply branch protection:' ) )
156
+ log( JSON.stringify( protectionPayload, null, 2 ) )
157
+ log( chalk.yellow( 'Will also set merge method to squash-only ' +
158
+ '(allow_squash_merge=true, allow_merge_commit=false, allow_rebase_merge=false), ' +
159
+ 'enable auto-delete of head branches on merge (delete_branch_on_merge=true), ' +
160
+ 'and enable the "Update branch" suggestion button on out-of-date PRs (allow_update_branch=true)' ) )
161
+
162
+ if ( opts.dryRun ) {
163
+ log( chalk.green( '\n[dry-run] No changes made.' ) )
164
+ process.exit( 0 )
165
+ }
166
+
167
+ await proceed( `\nApply this to ${fullRepo}@${branch}?` )
168
+
169
+ // ---------- Apply ----------
170
+
171
+ try {
172
+ await $( { input : JSON.stringify( protectionPayload ) } )`gh api --method PUT repos/${fullRepo}/branches/${branch}/protection --input -`
173
+ await $`gh api --method PATCH repos/${fullRepo} -F allow_squash_merge=true -F allow_merge_commit=false -F allow_rebase_merge=false -F delete_branch_on_merge=true -F allow_update_branch=true`
174
+ } catch ( error ) {
175
+ errorExit( `Failed to apply settings: ${error.stderr ?? error.message ?? error}` )
176
+ }
177
+
178
+ log( chalk.green( `\n✔ Branch protection applied to ${fullRepo}@${branch}` ) )
@@ -34,6 +34,9 @@ updates:
34
34
  ignore:
35
35
  - dependency-name: "eslint"
36
36
  versions: [">=10"]
37
+ groups:
38
+ patch-and-minor:
39
+ update-types: ["patch", "minor"]
37
40
  `
38
41
 
39
42
  program
@@ -11,6 +11,3 @@ config:
11
11
 
12
12
  # CNPG override
13
13
  SQL_HOST: "cnpg-db-psql-stack-pool-rw.cnpg-operands" # vs postgres-postgresql
14
-
15
- serviceMonitor:
16
- enabled: true
@@ -97,11 +97,11 @@ data:
97
97
  limits:
98
98
  cpu: 750m
99
99
  ephemeral-storage: 2Gi
100
- memory: 2Gi
100
+ memory: 8Gi
101
101
  requests:
102
102
  cpu: 500m
103
103
  ephemeral-storage: 50Mi
104
- memory: 1536Mi
104
+ memory: 4Gi
105
105
  persistence:
106
106
  enabled: true
107
107
  size: 20Gi # scale up based on expected data volume
@@ -114,7 +114,7 @@ ingest:
114
114
  limits:
115
115
  cpu: 750m
116
116
  ephemeral-storage: 2Gi
117
- memory: 512Mi
117
+ memory: 1536Mi
118
118
  requests:
119
119
  cpu: 250m
120
120
  ephemeral-storage: 50Mi
@@ -123,13 +123,13 @@ ingest:
123
123
  master:
124
124
  <<: *esScheduling
125
125
  replicaCount: 3
126
- heapSize: 256m
126
+ heapSize: 512m
127
127
  resources:
128
128
  limits:
129
129
  cpu: 500m
130
130
  ephemeral-storage: 2Gi
131
- memory: 768Mi
131
+ memory: 1536Mi
132
132
  requests:
133
133
  cpu: 250m
134
134
  ephemeral-storage: 50Mi
135
- memory: 512Mi
135
+ memory: 1Gi
@@ -3,6 +3,3 @@ image:
3
3
 
4
4
  config:
5
5
  LOG_CONFIG: '{"type":"pino","level":"warn"}'
6
-
7
- serviceMonitor:
8
- enabled: true
@@ -19,7 +19,7 @@ const SYM_GOOD = chalk.green( '✔' )
19
19
  const SYM_BAD = chalk.red( '✘' )
20
20
 
21
21
  // ---------- Checks ----------
22
- // Each check: { label: string, fn: ({ dir, pkg, isWorkspace }) => { value: string } }
22
+ // Each check: { label: string, fn: ({ dir, pkg, isWorkspace, gitRoot }) => { value: string } | Promise<{ value: string }> }
23
23
  // Add new checks here — the table expands automatically.
24
24
 
25
25
  let latestOrbVersion = null
@@ -83,14 +83,17 @@ const CHECKS = [
83
83
  },
84
84
  },
85
85
  {
86
- label : 'git',
87
- fn : ( { gitRoot } ) => {
86
+ label : 'bpr',
87
+ fn : async ( { gitRoot, isWorkspace } ) => {
88
+ if ( isWorkspace ) return { value : chalk.dim( '-' ) }
88
89
  try {
89
90
  const gitConfig = fs.readFileSync( path.join( gitRoot, '.git', 'config' ), 'utf8' )
90
- if ( /bitbucket/i.test( gitConfig ) ) return { value : chalk.red( 'bb' ) }
91
- if ( /github\.com/i.test( gitConfig ) ) return { value : chalk.green( 'gh' ) }
92
- return { value : chalk.dim( '?' ) }
93
- } catch {
91
+ const match = gitConfig.match( /github\.com[:/][^/]+\/(.+?)(?:\.git)?(?:\s|$)/m )
92
+ if ( !match ) return { value : chalk.dim( '?' ) }
93
+ await $`branch-protect-it --check ${match[1]}`
94
+ return { value : SYM_GOOD }
95
+ } catch ( err ) {
96
+ if ( err.exitCode === 1 ) return { value : SYM_BAD }
94
97
  return { value : chalk.dim( '?' ) }
95
98
  }
96
99
  },
@@ -263,7 +266,7 @@ async function checkRepo( dir, { pull } ) {
263
266
  dirty : !syncOk,
264
267
  scope : fmtScope( pkg ),
265
268
  isWorkspace,
266
- checks : CHECKS.map( ( { label, fn } ) => ( { label, ...fn( { dir, pkg, isWorkspace, gitRoot } ) } ) ),
269
+ checks : await Promise.all( CHECKS.map( async ( { label, fn } ) => ( { label, ...await fn( { dir, pkg, isWorkspace, gitRoot } ) } ) ) ),
267
270
  needsPush,
268
271
  }
269
272
  }
@@ -346,7 +349,7 @@ ${chalk.yellow( 'Columns:' )}
346
349
  ${chalk.green( 'ESM' )} "type": "module" in package.json
347
350
  ${chalk.green( 'har' )} .har directory present at repo root
348
351
  ${chalk.green( 'lnt' )} ESLint version from node_modules (✘=no lint script, ?=not installed, yellow=v8, green=v9+)
349
- ${chalk.green( 'git' )} origin remote: green gh=GitHub, red bb=Bitbucket
352
+ ${chalk.green( 'bpr' )} branch protection: ✔=standard bundle applied, ✘=missing/incomplete, -=workspace package, ?=not on GitHub
350
353
 
351
354
  ${chalk.yellow( 'Monorepos:' )}
352
355
  Workspace packages are discovered automatically — passing a monorepo root