@leverege/build-tools 2.121.0 → 2.123.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.121.0",
3
+ "version": "2.123.0",
4
4
  "description": "A collection of build / support tools for Leverege developers",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -25,6 +25,7 @@
25
25
  "chart-compass": "src/chart-compass.mjs",
26
26
  "chart-to-registry": "src/chart-to-registry.mjs",
27
27
  "clone-cnpg-from-snapshot": "src/clone-cnpg-from-snapshot.mjs",
28
+ "close-dependabot-prs": "src/close-dependabot-prs.mjs",
28
29
  "count-lines-of-code": "src/count-lines-of-code.mjs",
29
30
  "dashboard": "src/dashboard/dashboard.mjs",
30
31
  "decrypt-secrets": "src/decrypt-secrets.sh",
@@ -103,14 +104,14 @@
103
104
  "glob": "^13.0.6",
104
105
  "googleapis": "^176.0.0",
105
106
  "handlebars": "^4.7.9",
106
- "ignore": "^7.0.6",
107
- "inquirer": "^14.2.0",
108
- "js-yaml": "^4.2.0",
107
+ "ignore": "^7.0.9",
108
+ "inquirer": "^14.2.2",
109
+ "js-yaml": "^4.3.2",
109
110
  "jsdoc": "^4.0.5",
110
111
  "ms": "^2.1.3",
111
112
  "npm-registry-fetch": "^20.0.1",
112
113
  "ora": "^9.4.1",
113
- "p-limit": "^7.3.1",
114
+ "p-limit": "^7.3.2",
114
115
  "package-up": "^5.0.0",
115
116
  "readline-sync": "^1.4.10",
116
117
  "semver": "^7.8.5",
@@ -127,7 +128,7 @@
127
128
  "@leverege/eslint-config-leverege": "^9",
128
129
  "chai": "^6.2.2",
129
130
  "eslint": "^9",
130
- "mocha": "^11.8.0",
131
+ "mocha": "^12.0.0",
131
132
  "npm": "^12.0.2"
132
133
  },
133
134
  "allowScripts": {
@@ -85,6 +85,22 @@ const LEVEREGE_REGISTRY = 'us-docker.pkg.dev/leverege-registry/system/images/lev
85
85
  const LEVEREGE_IMAGE_TAG = `${PG_VERSION}-cnpg-lvrg-${subTag}`
86
86
  const LEVEREGE_IMAGE_NAME_WITH_TAG = `${LEVEREGE_REGISTRY}:${LEVEREGE_IMAGE_TAG}`
87
87
 
88
+ const CLOUDBUILD_YAML_TEMPLATE = `steps:
89
+ - name: 'gcr.io/cloud-builders/docker'
90
+ entrypoint: bash
91
+ args:
92
+ - '-c'
93
+ - |
94
+ docker run --privileged --rm tonistiigi/binfmt --install all
95
+ docker buildx create --use --name builder --driver docker-container
96
+ docker buildx inspect --bootstrap
97
+ docker buildx build \\
98
+ --platform linux/amd64,linux/arm64 \\
99
+ --tag {{IMAGE_TAG}} \\
100
+ --push \\
101
+ /workspace
102
+ `
103
+
88
104
  const DOCKERFILE_TEMPLATE = `
89
105
  FROM ghcr.io/cloudnative-pg/postgresql:{{PG_VERSION}}-bookworm
90
106
 
@@ -130,22 +146,27 @@ const checkExistingImage = async ( imageName ) => {
130
146
 
131
147
  // Build and push Docker image
132
148
  const buildAndPushImage = async () => {
133
- const template = Handlebars.compile( DOCKERFILE_TEMPLATE )
134
- const renderedDockerfile = template( { PG_VERSION } )
135
- debug( { renderedDockerfile }, '<==Dockerfile' )
149
+ const dockerfileTemplate = Handlebars.compile( DOCKERFILE_TEMPLATE )
150
+ const cloudbuildTemplate = Handlebars.compile( CLOUDBUILD_YAML_TEMPLATE )
151
+
152
+ const renderedDockerfile = dockerfileTemplate( { PG_VERSION } )
153
+ const renderedCloudbuild = cloudbuildTemplate( { IMAGE_TAG : LEVEREGE_IMAGE_NAME_WITH_TAG } )
154
+ debug( { renderedDockerfile, renderedCloudbuild }, '<==Build files' )
136
155
 
137
156
  // Create a temporary directory to build from
138
157
  const buildDir = fs.mkdtempSync( path.join( os.tmpdir(), 'docker-build-' ) )
139
158
  const dockerfilePath = path.join( buildDir, 'Dockerfile' )
159
+ const cloudbuildPath = path.join( buildDir, 'cloudbuild.yaml' )
140
160
 
141
161
  try {
142
162
  fs.writeFileSync( dockerfilePath, renderedDockerfile ) // eslint-disable-line security/detect-non-literal-fs-filename
163
+ fs.writeFileSync( cloudbuildPath, renderedCloudbuild ) // eslint-disable-line security/detect-non-literal-fs-filename
143
164
 
144
- log( chalk.yellow( '\nBuilding Docker image...' ) )
165
+ log( chalk.yellow( '\nBuilding Docker image (multi-arch: linux/amd64, linux/arm64)...' ) )
145
166
  const cloudBuildSubmit = 'gcloud builds submit --project leverege-registry'
146
167
  const cloudBuildLog = '--gcs-log-dir gs://leverege-registry_cloudbuild/log'
147
- const imageTag = `--tag ${LEVEREGE_IMAGE_NAME_WITH_TAG}`
148
- await shellCmd( `${cloudBuildSubmit} ${cloudBuildLog} ${imageTag} ${buildDir}`, { stdio : 'inherit' } )
168
+ const configFlag = `--config ${cloudbuildPath}`
169
+ await shellCmd( `${cloudBuildSubmit} ${cloudBuildLog} ${configFlag} ${buildDir}`, { stdio : 'inherit' } )
149
170
  } finally {
150
171
  // Ensure cleanup even if the build fails
151
172
  fs.rmSync( buildDir, { recursive : true, force : true } )
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { $ } from 'zx'
4
+ import { program } from 'commander'
5
+ import chalk from 'chalk'
6
+
7
+ import { enableDebug, errorExit, log } from './Utils.mjs'
8
+
9
+ $.verbose = false
10
+
11
+ const DEFAULT_OWNER = 'Leverege'
12
+
13
+ const COMMENT = (
14
+ 'Closing in favor of the new grouped Dependabot config (patch/minor ' +
15
+ 'updates now batch into a single PR). Not a rejection of this specific ' +
16
+ 'bump - Dependabot will re-propose it as part of a grouped PR on its ' +
17
+ 'next scan.'
18
+ )
19
+
20
+ const TITLE_RE = /^Bump (\S+) from (\S+) to (\S+)$/
21
+
22
+ program
23
+ .name( 'close-dependabot-prs' )
24
+ .description( chalk.green( 'Close open Dependabot PRs superseded by a newly-grouped dependabot.yml config' ) )
25
+ .argument( '<repos...>', 'repo names - bare name uses --owner, or pass owner/repo to override' )
26
+ .option( '--owner <name>', 'GitHub org/owner for bare repo names', DEFAULT_OWNER )
27
+ .option( '--dry-run', 'show what would be closed without closing anything' )
28
+ .option( '--debug', 'Enable debug logging' )
29
+ .addHelpText( 'after', `
30
+ ${chalk.yellow( 'Examples:' )}
31
+ ${chalk.green( 'close-dependabot-prs array-util base62-util' )}
32
+ ${chalk.green( 'close-dependabot-prs --dry-run cache' )}
33
+ ${chalk.green( 'close-dependabot-prs --owner SomeOtherOrg some-repo' )}
34
+
35
+ ${chalk.yellow( 'Notes:' )}
36
+ Closes open Dependabot PRs that are superseded by a newly-added groups:
37
+ config in dependabot.yml (patch/minor updates now batch into one PR).
38
+ Leaves major-version PRs open, since they aren't covered by grouping and
39
+ closing them risks Dependabot never re-proposing that exact version.
40
+
41
+ Skips any repo whose dependabot.yml has no groups: block at all - closing
42
+ PRs there would lose them rather than get them re-proposed.
43
+
44
+ Requires the gh CLI, authenticated.
45
+ ` )
46
+ .parse()
47
+
48
+ const opts = program.opts()
49
+ if ( opts.debug ) { enableDebug(); $.verbose = true }
50
+
51
+ const repos = program.args.map( repo => ( repo.includes( '/' ) ? repo : `${opts.owner}/${repo}` ) )
52
+
53
+ if ( repos.length === 0 ) {
54
+ errorExit( 'No repos provided.' )
55
+ }
56
+
57
+ // ---------- Helpers ----------
58
+
59
+ async function hasGroupingConfigured( repo ) {
60
+ let stdout
61
+ try {
62
+ ( { stdout } = await $`gh api repos/${repo}/contents/.github/dependabot.yml --jq .content` )
63
+ } catch {
64
+ return null // couldn't fetch - treat as unknown, caller should skip
65
+ }
66
+ const content = Buffer.from( stdout.trim(), 'base64' ).toString( 'utf8' )
67
+ return content.includes( 'groups:' )
68
+ }
69
+
70
+ function versionTuple( v ) {
71
+ const m = v.match( /^(\d+(?:\.\d+)*)/ )
72
+ if ( !m ) return null
73
+ return m[1].split( '.' ).map( Number )
74
+ }
75
+
76
+ function classifyBump( fromV, toV ) {
77
+ const a = versionTuple( fromV )
78
+ const b = versionTuple( toV )
79
+ if ( !a || !b ) return 'unknown'
80
+ const length = Math.max( a.length, b.length )
81
+ while ( a.length < length ) a.push( 0 )
82
+ while ( b.length < length ) b.push( 0 )
83
+ if ( a[0] !== b[0] ) return 'major'
84
+ if ( a.length > 1 && a[1] !== b[1] ) return 'minor'
85
+ return 'patch'
86
+ }
87
+
88
+ async function openDependabotPrs( repo ) {
89
+ const { stdout } = await $`gh pr list --repo ${repo} --state open --search head:dependabot --json number,title,headRefName`
90
+ return JSON.parse( stdout )
91
+ }
92
+
93
+ async function processRepo( repo, dryRun ) {
94
+ log( `\n=== ${repo} ===` )
95
+ const grouped = await hasGroupingConfigured( repo )
96
+ if ( grouped === null ) {
97
+ log( ' ! could not read .github/dependabot.yml - skipping' )
98
+ return
99
+ }
100
+ if ( !grouped ) {
101
+ log(
102
+ ' ! no `groups:` block found in dependabot.yml - skipping ' +
103
+ '(closing PRs here would lose them, not get them re-proposed)'
104
+ )
105
+ return
106
+ }
107
+
108
+ const prs = await openDependabotPrs( repo )
109
+ if ( prs.length === 0 ) {
110
+ log( ' no open dependabot PRs' )
111
+ return
112
+ }
113
+
114
+ let closedCount = 0
115
+ let keptCount = 0
116
+ let skippedCount = 0
117
+
118
+ for ( const pr of prs ) {
119
+ const m = pr.title.match( TITLE_RE )
120
+ if ( !m ) {
121
+ skippedCount++
122
+ log( ` skip #${pr.number} (title didn't match expected pattern) ${pr.title}` )
123
+ continue
124
+ }
125
+ const [ , , fromV, toV ] = m
126
+ const bumpType = classifyBump( fromV, toV )
127
+ if ( bumpType === 'patch' || bumpType === 'minor' ) {
128
+ closedCount++
129
+ log( ` close #${String( pr.number ).padEnd( 4 )} ${bumpType.padEnd( 7 )} ${pr.title}` )
130
+ if ( !dryRun ) {
131
+ // eslint-disable-next-line no-await-in-loop
132
+ await $`gh pr close ${pr.number} --repo ${repo} --comment ${COMMENT}`
133
+ }
134
+ } else {
135
+ keptCount++
136
+ log( ` keep #${String( pr.number ).padEnd( 4 )} ${bumpType.padEnd( 7 )} ${pr.title}` )
137
+ }
138
+ }
139
+
140
+ log( ` -> ${closedCount} closed, ${keptCount} left open (major), ${skippedCount} skipped` )
141
+ }
142
+
143
+ // ---------- Run ----------
144
+
145
+ for ( const repo of repos ) {
146
+ // eslint-disable-next-line no-await-in-loop
147
+ await processRepo( repo, opts.dryRun )
148
+ }