@leverege/build-tools 2.116.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.116.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",
package/src/Docker.mjs CHANGED
@@ -148,6 +148,10 @@ const generateDockerfile = async ( options ) => {
148
148
  if ( fs.existsSync( dockerfilePrePlugin ) ) {
149
149
  settings.preInstallPluginfile = fs.readFileSync( dockerfilePrePlugin )
150
150
  }
151
+ const dockerfileFinalStagePrePlugin = './docker/DockerfileFinalStagePreInstall.plugin'
152
+ if ( fs.existsSync( dockerfileFinalStagePrePlugin ) ) {
153
+ settings.finalStagePreInstallPluginfile = fs.readFileSync( dockerfileFinalStagePrePlugin )
154
+ }
151
155
 
152
156
  const compiled = handlebars.compile( dockerfileTemplate, { noEscape : true } )
153
157
  const replaced = compiled( settings )
@@ -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}` ) )
@@ -129,7 +129,7 @@ const dockerfileOptions = {
129
129
  isNpmWorkspace,
130
130
  buildEnv : leveregeStanza.buildEnv || leveregeStanza['build-env'] || 'alpine',
131
131
  imageBase : leveregeStanza.imageBase || leveregeStanza['image-base'],
132
- runuser : leveregeStanza.runUser || leveregeStanza['run-user'] || isNodeJsProject ? 'node' : 'python',
132
+ runuser : leveregeStanza.runUser || leveregeStanza['run-user'] || ( isNodeJsProject ? 'node' : 'python' ),
133
133
  nodeimage : leveregeStanza.nodeimg, // left as nodeimg in package.json for backwards compat
134
134
  regvers : repoDescr.packageVersion,
135
135
  pythonVersion : repoDescr.rootPyProjectToml?.pythonVersion,
@@ -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
@@ -70,6 +70,7 @@ RUN npm ci --only=production --no-optional {{npmlogging}} && \
70
70
  # --------------------------------------------------------------
71
71
 
72
72
  FROM {{image}}
73
+ {{finalStagePreInstallPluginfile}}
73
74
 
74
75
  ARG BUILD_ENV="{{buildEnv}}"
75
76
 
@@ -97,7 +98,7 @@ RUN if [ "$BUILD_ENV" = "debian" ]; then \
97
98
  npm install -g {{npmVersion}} && \
98
99
  {{/if}}
99
100
  rm /bin/sh && ln -s /bin/bash /bin/sh && \
100
- mkdir -p /usr/src/app /tmp/levlog && chown node:node /usr/src/app; \
101
+ mkdir -p /usr/src/app /tmp/levlog && chown {{runuser}}:{{runuser}} /usr/src/app; \
101
102
  else \
102
103
  apk update && \
103
104
  apk add --no-cache bash curl tini vim {{apkadds}} && \
@@ -108,7 +109,7 @@ RUN if [ "$BUILD_ENV" = "debian" ]; then \
108
109
  npm install -g {{npmVersion}} && \
109
110
  {{/if}}
110
111
  rm /bin/sh && ln -s /bin/bash /bin/sh && \
111
- mkdir -p /usr/src/app /tmp/levlog && chown node:node /usr/src/app; \
112
+ mkdir -p /usr/src/app /tmp/levlog && chown {{runuser}}:{{runuser}} /usr/src/app; \
112
113
  fi
113
114
 
114
115
 
@@ -125,8 +126,8 @@ RUN rm -f ${NPM_CONFIG_USERCONFIG}
125
126
  ENTRYPOINT [ "tini", "--" ]
126
127
  WORKDIR /usr/src/app
127
128
 
128
- COPY --chown=node:node --from=intermediate /usr/src/app /usr/src/app
129
+ COPY --chown={{runuser}}:{{runuser}} --from=intermediate /usr/src/app /usr/src/app
129
130
 
130
131
  USER {{runuser}}
131
- COPY ./bashrc /home/node/.bashrc
132
- CMD [ "/bin/bash", "-c", "source /home/node/.bashrc && {{runCommand}}" ]
132
+ COPY --chown={{runuser}}:{{runuser}} ./bashrc /home/{{runuser}}/.bashrc
133
+ CMD [ "/bin/bash", "-c", "source /home/{{runuser}}/.bashrc && {{runCommand}}" ]