@leverege/build-tools 2.94.0-quinn.8 → 2.94.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.94.0-quinn.8",
3
+ "version": "2.94.0",
4
4
  "description": "A collection of build / support tools for Leverege developers",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -11,13 +11,16 @@
11
11
  "clean": "rm -rf lib",
12
12
  "lint": "find ./src -name \\*.\\*js | xargs npx eslint --report-unused-disable-directives",
13
13
  "prepare": "[ $SKIP_PREPARE ] && exit 0 || ./src/hook-and-release.mjs",
14
- "test": "mocha -t 60000 --exit 'test/**/*.mjs'",
14
+ "test": "mocha -t 60000 --exit 'test/**/!(*e2e*).mjs'",
15
+ "test-all": "mocha -t 60000 --exit 'test/**/**.mjs'",
15
16
  "prepack": "npm exec prepack"
16
17
  },
17
18
  "bin": {
18
19
  "build-and-push": "src/build-and-push.sh",
19
20
  "build-cnpg-image": "src/build-cnpg-image.mjs",
20
21
  "build-tools": "src/build-tools.mjs",
22
+ "change-up": "src/change-up.sh",
23
+ "chart-compass": "src/chart-compass.mjs",
21
24
  "chart-to-museum": "src/chart-to-museum.mjs",
22
25
  "chart-to-registry": "src/chart-to-registry.mjs",
23
26
  "circleate": "src/circleate.sh",
@@ -64,7 +67,8 @@
64
67
  "snapshot-cnpg": "src/snapshot-cnpg.mjs",
65
68
  "tag-release": "src/tag-release.mjs",
66
69
  "unleash": "src/unleash.mjs",
67
- "upload-model": "src/upload-model.sh"
70
+ "upload-model": "src/upload-model.sh",
71
+ "yarn-build": "src/yarn-build.mjs"
68
72
  },
69
73
  "author": "Leverege Devs",
70
74
  "license": "SEE LICENSE IN LICENSE.md",
@@ -76,11 +80,12 @@
76
80
  "commander": "^14.0.2",
77
81
  "deepmerge": "^4.3.1",
78
82
  "enquirer": "^2.4.1",
79
- "execa": "^9.6.0",
80
- "glob": "^11.0.3",
83
+ "execa": "^9.6.1",
84
+ "find-yarn-workspace-root": "^2.0.0",
85
+ "glob": "^13.0.0",
81
86
  "handlebars": "^4.7.8",
82
87
  "ignore": "^7.0.5",
83
- "inquirer": "^12.11.1",
88
+ "inquirer": "^13.1.0",
84
89
  "js-yaml": "^4.1.1",
85
90
  "jsdoc": "^4.0.5",
86
91
  "ms": "^2.1.3",
@@ -101,6 +106,6 @@
101
106
  "@leverege/eslint-config-leverege": "^5.1.1",
102
107
  "chai": "^6.2.1",
103
108
  "mocha": "^11.7.5",
104
- "npm": "^11.6.2"
109
+ "npm": "^11.7.0"
105
110
  }
106
111
  }
package/src/Docker.mjs CHANGED
@@ -17,8 +17,14 @@ import {
17
17
 
18
18
  const DIRNAME = path.dirname( url.fileURLToPath( import.meta.url ) )
19
19
 
20
- // TODO: change default to node 22 (jod) after 01/01/25
21
- const dockerfileNodeVersion = 'jod-alpine'
20
+ // TODO: This is a temporary band-aid while we assess the impact of the yarn
21
+ // and/or CI pipeline transition. This will at least allow us to lock down the
22
+ // specific versions of node/npm and facilitate testing via the env vars.
23
+ //
24
+ // See the latest docker images here => https://hub.docker.com/_/node
25
+ //
26
+ const dockerfileNodeVersion = process.env.DOCKER_BUILD_NODE_VERSION || '22.21.1-alpine3.23' // TODO: 1/1/26 => '24.12.0-alpine3.23'
27
+ const dockerfileNpmVersion = process.env.DOCKER_BUILD_NPM_VERSION || '11.6.0'
22
28
 
23
29
  // The contents of these variables were initially located in files that live in
24
30
  // the build-tools repository, but it just became simpler to pull the contents
@@ -55,7 +61,7 @@ const defaultSettings = {
55
61
  nodeimage : 'jod-alpine',
56
62
  imageBase : 'node:jod-alpine',
57
63
  npmlogging : process.env.VERBOSE_NPM_LOGGING ? '--ddd' : '--silent',
58
- npmVersion : 'npm@11',
64
+ npmVersion : `npm@${dockerfileNpmVersion}`,
59
65
  pluginfile : '# NO PLUGIN',
60
66
  preInstallPluginfile : '# NO PRE-INSTALL PLUGIN',
61
67
  regvers : 'package.version',
package/src/Utils.mjs CHANGED
@@ -493,7 +493,7 @@ export const parseHelmChart = async ( helmroot = './helm' ) => {
493
493
 
494
494
  // See if this is a leaf chart and whether or not dependencies need updating
495
495
  const hasDependencies = chartYaml.dependencies !== undefined
496
- let depsOutOfSync
496
+ let depsOutOfSync = hasDependencies // assume unsynced if dependencies
497
497
  if ( chartFiles.includes( 'Chart.lock' ) ) {
498
498
  const chartLock = YAML.load( fs.readFileSync( `${helmroot}/Chart.lock`, 'utf8' ) )
499
499
  // Using Object.fromEntries will be able to handle multiple dependencies if ever needed
@@ -1,12 +1,14 @@
1
1
  /* eslint-disable security/detect-non-literal-fs-filename */
2
2
  import fs from 'node:fs'
3
+ import path from 'node:path'
3
4
 
4
5
  import { shellCmd, log, debug, err } from '../Utils.mjs'
5
6
 
6
- import { toExplicitPath, deduplicatePath, convertGenericArtifactNameToPath } from './Utils.mjs'
7
+ import { parseGenericArtifactSchemaToRemoteRelativePath, parseGenericArtifactSchemaToLocalRelativePath, listGenericArtifactFiles, parseGenericArtifactFileToRemoteRelativePath, parseGenericArtifactSchemaToArtifactName } from './Utils.mjs'
7
8
 
8
9
  class ArtifactPuller {
9
- constructor( artifact ) {
10
+ constructor( path, artifact ) {
11
+ this.path = path
10
12
  this.artifact = artifact
11
13
  }
12
14
 
@@ -17,142 +19,101 @@ class ArtifactPuller {
17
19
 
18
20
  class GenericArtifactPuller extends ArtifactPuller {
19
21
 
20
- async prepareFileDownload( tmpFilePath, finalFilePath ) {
21
- // Ensure directories exist
22
- fs.mkdirSync( tmpFilePath.split( '/' ).slice( 0, -1 ).join( '/' ), { recursive : true } )
23
- fs.mkdirSync( finalFilePath.split( '/' ).slice( 0, -1 ).join( '/' ), { recursive : true } )
22
+ async downloadFile( remoteRelativePath, localRelativePath ) {
23
+ debug( { remoteRelativePath, localRelativePath }, '<==Download File' )
24
24
 
25
- // Ensure no conflicting files exist
26
- if ( fs.existsSync( tmpFilePath ) ) {
27
- fs.unlinkSync( tmpFilePath )
28
- }
29
- if ( fs.existsSync( finalFilePath ) ) {
30
- fs.unlinkSync( finalFilePath )
31
- }
32
- }
25
+ const isBasenameDifferent = path.basename( localRelativePath ) !== path.basename( remoteRelativePath )
33
26
 
34
- async cleanupFileDownload( tmpFilePath, finalFilePath ) {
35
- // Delete the tmp file
36
- if ( fs.existsSync( tmpFilePath ) ) {
37
- fs.unlinkSync( tmpFilePath )
27
+ let tmpLocalRelativePath
28
+ if ( isBasenameDifferent ) {
29
+ const artifactName = parseGenericArtifactSchemaToArtifactName( this.artifact )
30
+ const encodedArtifactName = artifactName.replaceAll( ':', '_' ).replaceAll( '/', '_' ).replaceAll( '%2F', '_' )
31
+ tmpLocalRelativePath = path.join( '/', 'tmp', encodedArtifactName )
32
+ } else {
33
+ await fs.promises.mkdir( path.dirname( localRelativePath ), { recursive : true } )
38
34
  }
39
- // Delete the tmp directory
40
- if ( fs.existsSync( tmpFilePath.split( '/' ).slice( 0, -1 ).join( '/' ) ) ) {
41
- fs.rmSync( tmpFilePath.split( '/' ).slice( 0, -1 ).join( '/' ), { recursive : true } )
42
- }
43
- }
44
-
45
- async downloadFile( remoteFilePath, localFilePath ) {
46
- debug( { remoteFilePath, localFilePath }, '<==Download File' )
47
-
48
- // 1 dir per file to avoid dir creation and deletion race conditions for files in the same directory
49
- const tmpDirectoryPath = `/tmp/${remoteFilePath.replaceAll( '/', '_' ).replaceAll( '.', '_' )}`
50
- const tmpFilePath = `${tmpDirectoryPath}/${remoteFilePath.split( '/' ).pop()}`
51
-
52
- await this.prepareFileDownload( tmpFilePath, localFilePath )
53
35
 
54
36
  try {
55
37
  const command = `gcloud artifacts generic download \
56
- --project=${this.artifact.gcp_project_id} \
57
- --location=${this.artifact.gcp_location} \
58
- --repository=${this.artifact.gcp_artifact_registry_repository_name} \
59
- --package=${this.artifact.gcp_artifact_registry_package_name} \
60
- --version=${this.artifact.gcp_artifact_registry_package_version} \
61
- --name="${remoteFilePath}" \
62
- --destination=${tmpDirectoryPath}`
38
+ --project=${this.artifact.project} \
39
+ --location=${this.artifact.location} \
40
+ --repository=${this.artifact.repository} \
41
+ --package=${this.artifact.package} \
42
+ --version=${this.artifact.version} \
43
+ --name="${remoteRelativePath}" \
44
+ --destination=${tmpLocalRelativePath || path.dirname( localRelativePath )}`
63
45
 
64
46
  debug( { command }, '<==Download File Command' )
65
47
 
66
48
  await shellCmd( command )
67
49
 
68
- // Check if file exists in the tmp path
69
- if ( !fs.existsSync( tmpFilePath ) ) {
70
- debug( { tmpFilePath }, '<==File does not exist in the tmp path' )
71
- return false
72
- }
73
-
74
- debug( { tmpFilePath, localFilePath }, '<==Copying file' )
75
-
76
- await fs.copyFileSync( tmpFilePath, localFilePath )
77
-
78
- // Check if file exists in the local path
79
- if ( !fs.existsSync( localFilePath ) ) {
80
- debug( { localFilePath }, '<==File does not exist in the local path' )
81
- return false
50
+ if ( isBasenameDifferent ) {
51
+ await fs.promises.rename( tmpLocalRelativePath, localRelativePath )
82
52
  }
83
53
 
84
54
  return true
85
55
  } catch ( error ) {
86
- err( `Error downloading file: ${remoteFilePath} ${error.message}` )
56
+ err( `Error downloading file: ${localRelativePath} ${error.message}` )
87
57
  return false
88
- } finally {
89
- await this.cleanupFileDownload( tmpFilePath, localFilePath )
90
58
  }
91
59
  }
92
60
 
93
- async downloadDirectory( localPath ) {
61
+ async downloadDirectory( remoteRelativePath, localRelativePath ) {
62
+
63
+ await fs.promises.mkdir( localRelativePath, { recursive : true } )
64
+
94
65
  const command = `gcloud artifacts generic download \
95
- --project=${this.artifact.gcp_project_id} \
96
- --location=${this.artifact.gcp_location} \
97
- --repository=${this.artifact.gcp_artifact_registry_repository_name} \
98
- --package=${this.artifact.gcp_artifact_registry_package_name} \
99
- --version=${this.artifact.gcp_artifact_registry_package_version} \
100
- --destination=${localPath}`
66
+ --project=${this.artifact.project} \
67
+ --location=${this.artifact.location} \
68
+ --repository=${this.artifact.repository} \
69
+ --package=${this.artifact.package} \
70
+ --version=${this.artifact.version} \
71
+ --destination=${localRelativePath}`
101
72
 
102
73
  debug( { command }, '<==Download Directory Command' )
103
74
 
104
75
  await shellCmd( command )
105
76
  }
106
77
 
107
- async downloadNestedDirectory( remotePath, localPath ) {
108
- debug( { remotePath, localPath }, '<==Download Nested Directory' )
78
+ async downloadNestedDirectory( remoteRelativePath, localRelativePath ) {
79
+ debug( { remoteRelativePath, localRelativePath }, '<==Download Nested Directory' )
109
80
 
110
- const files = await this.listFiles( remotePath )
81
+ const files = await listGenericArtifactFiles( this.artifact )
111
82
  await Promise.all(
112
83
  files
113
- .filter( file => convertGenericArtifactNameToPath( file.name ).startsWith( remotePath ) )
114
- .map( async ( file ) => {
115
- const remoteFilePath = convertGenericArtifactNameToPath( file.name )
116
- const localFilePath = deduplicatePath( `${localPath}/${remoteFilePath.replaceAll( remotePath, '' )}` )
117
- await this.downloadFile( remoteFilePath, localFilePath )
118
- } )
84
+ .reduce( ( acc, file ) => {
85
+ const remoteFilePath = parseGenericArtifactFileToRemoteRelativePath( file )
86
+ if ( remoteFilePath.startsWith( remoteRelativePath ) ) {
87
+ const remoteFilePathWithoutRemotePath = remoteFilePath.replace( remoteRelativePath, '' )
88
+ const localFilePath = path.join( localRelativePath, remoteFilePathWithoutRemotePath )
89
+ acc.push( this.downloadFile( remoteFilePath, localFilePath ) )
90
+ }
91
+ return acc
92
+ }, [] )
119
93
  )
120
94
  }
121
95
 
122
- async listFiles( ) {
123
- const command = `gcloud artifacts files list \
124
- --project=${this.artifact.gcp_project_id} \
125
- --location=${this.artifact.gcp_location} \
126
- --repository=${this.artifact.gcp_artifact_registry_repository_name} \
127
- --package=${this.artifact.gcp_artifact_registry_package_name} \
128
- --version ${this.artifact.gcp_artifact_registry_package_version} \
129
- --format="json"`
130
-
131
- debug( { command }, '<==List Files Command' )
132
-
133
- const result = await shellCmd( command )
134
-
135
- // Parse the JSON response
136
- const files = JSON.parse( result )
137
- return files
138
- }
139
-
140
96
  async pull() {
141
97
  log( `Pulling artifact: ${this.artifact.name}` )
142
98
 
143
99
  debug( { artifact : this.artifact }, '<==Pulling artifact' )
144
100
 
145
101
  try {
102
+ const remoteRelativePath = parseGenericArtifactSchemaToRemoteRelativePath( this.artifact )
103
+ const localRelativePath = parseGenericArtifactSchemaToLocalRelativePath( this.artifact )
104
+
146
105
  if ( this.artifact.remote_file_path && this.artifact.local_file_path ) {
147
- await this.downloadFile( this.artifact.remote_file_path, toExplicitPath( this.artifact.local_file_path ) )
106
+ await this.downloadFile( remoteRelativePath, path.join( this.path, localRelativePath ) )
148
107
  } else if ( this.artifact.remote_file_path && this.artifact.local_path ) {
149
- await this.downloadFile( this.artifact.remote_file_path, toExplicitPath( `${this.artifact.local_path}/${this.artifact.remote_file_path.split( '/' ).pop()}` ) )
108
+ await this.downloadFile( remoteRelativePath, path.join( this.path, localRelativePath ) )
109
+ } else if ( this.artifact.remote_file_path && !this.artifact.local_path && !this.artifact.local_file_path ) {
110
+ await this.downloadFile( remoteRelativePath, path.join( this.path, localRelativePath ) )
150
111
  } else if ( this.artifact.remote_path && this.artifact.local_path ) {
151
- await this.downloadNestedDirectory( this.artifact.remote_path, toExplicitPath( this.artifact.local_path ) )
112
+ await this.downloadNestedDirectory( remoteRelativePath, path.join( this.path, localRelativePath ) )
152
113
  } else if ( this.artifact.remote_path && !this.artifact.local_path && !this.artifact.local_file_path ) {
153
- await this.downloadDirectory( this.artifact.remote_path, '.' )
114
+ await this.downloadDirectory( remoteRelativePath, path.join( this.path, localRelativePath ) )
154
115
  } else if ( !this.artifact.remote_path && !this.artifact.remote_file_path && this.artifact.local_path ) {
155
- await this.downloadDirectory( toExplicitPath( this.artifact.local_path ) )
116
+ await this.downloadDirectory( remoteRelativePath, path.join( this.path, localRelativePath ) )
156
117
  } else {
157
118
  throw new Error( 'No valid paths provided' )
158
119
  }
@@ -160,13 +121,6 @@ class GenericArtifactPuller extends ArtifactPuller {
160
121
  log( `Successfully pulled artifact: ${this.artifact.name}` )
161
122
 
162
123
  return true
163
-
164
- // if ( !this.artifact.remote_path && !this.artifact.remote_file_path && this.local_file_path ) {
165
- // throw new Error( '' )
166
- // }
167
- // if ( this.artifact.remote_path && this.artifact.local_file_path ) {
168
- // throw new Error( '' )
169
- // }
170
124
  } catch ( error ) {
171
125
  err( `Error pulling artifact: ${this.artifact.name} ${error.message}` )
172
126
  }
@@ -1,9 +1,20 @@
1
+ import path from 'node:path'
2
+ import fs from 'node:fs'
3
+
1
4
  import { shellCmd, log, err, debug } from '../Utils.mjs'
2
5
 
3
- import { convertGenericArtifactNameToPath } from './Utils.mjs'
6
+ import {
7
+ parseGenericArtifactFileToRemoteRelativePath,
8
+ parseGenericArtifactSchemaToRemoteRelativePath,
9
+ listGenericArtifactFiles,
10
+ shouldUseIgnoreConfiguration,
11
+ findIgnoreFilePath,
12
+ copyFilesWithIgnoreConfiguration
13
+ } from './Utils.mjs'
4
14
 
5
15
  class ArtifactPusher {
6
- constructor( artifact ) {
16
+ constructor( path, artifact ) {
17
+ this.path = path
7
18
  this.artifact = artifact
8
19
  }
9
20
 
@@ -13,25 +24,6 @@ class ArtifactPusher {
13
24
  }
14
25
 
15
26
  class GenericArtifactPusher extends ArtifactPusher {
16
-
17
- async listFiles( ) {
18
- const command = `gcloud artifacts files list \
19
- --project=${this.artifact.gcp_project_id} \
20
- --location=${this.artifact.gcp_location} \
21
- --repository=${this.artifact.gcp_artifact_registry_repository_name} \
22
- --package=${this.artifact.gcp_artifact_registry_package_name} \
23
- --version ${this.artifact.gcp_artifact_registry_package_version} \
24
- --format="json"`
25
-
26
- debug( { command }, '<==List Files Command' )
27
-
28
- const result = await shellCmd( command )
29
-
30
- // Parse the JSON response
31
- const files = JSON.parse( result )
32
-
33
- return files
34
- }
35
27
 
36
28
  async push() {
37
29
  log( `Pushing artifact: ${this.artifact.name}` )
@@ -41,41 +33,74 @@ class GenericArtifactPusher extends ArtifactPusher {
41
33
  return false
42
34
  }
43
35
 
44
- try {
45
-
36
+ if ( !this.artifact.local_path && !this.artifact.local_file_path ) {
37
+ err( 'Local path or local file path is required for pushing artifacts' )
38
+ return false
39
+ }
40
+
41
+ try {
46
42
  let command = ''
47
43
 
48
44
  command = `gcloud artifacts generic upload \
49
- --project=${this.artifact.gcp_project_id} \
50
- --location=${this.artifact.gcp_location} \
51
- --repository=${this.artifact.gcp_artifact_registry_repository_name} \
52
- --package=${this.artifact.gcp_artifact_registry_package_name} \
53
- --version=${this.artifact.gcp_artifact_registry_package_version}`
45
+ --project=${this.artifact.project} \
46
+ --location=${this.artifact.location} \
47
+ --repository=${this.artifact.repository} \
48
+ --package=${this.artifact.package} \
49
+ --version=${this.artifact.version}`
50
+
51
+ let pathToDelete
54
52
 
55
53
  if ( this.artifact.local_path ) {
56
- command += ` --source-directory=${this.artifact.local_path}`
54
+ const originalLocalPath = path.join( this.path, this.artifact.local_path )
55
+
56
+ let localPath = originalLocalPath
57
+
58
+ const useIgnoreConfiguration = await shouldUseIgnoreConfiguration()
59
+
60
+ if ( useIgnoreConfiguration ) {
61
+ const ignoreFilePath = await findIgnoreFilePath( this.path )
62
+ if ( ignoreFilePath ) {
63
+ const tmpLocalPath = path.join( '/', 'tmp', `${this.artifact.project}_${this.artifact.location}_${this.artifact.repository}_${this.artifact.package}_${this.artifact.version}` )
64
+ localPath = tmpLocalPath
65
+ pathToDelete = tmpLocalPath
66
+ await copyFilesWithIgnoreConfiguration( originalLocalPath, tmpLocalPath, ignoreFilePath )
67
+ }
68
+ }
69
+
70
+ command += ` --source-directory=${localPath}`
57
71
  command += ' --skip-existing'
58
72
  } else if ( this.artifact.local_file_path ) {
59
- const files = await this.listFiles( this.artifact.remote_path )
60
- if ( files.some( file => convertGenericArtifactNameToPath( file.name ) === `${this.artifact.remote_path}/${this.artifact.remote_file_path.split( '/' ).pop()}` ) ) {
61
- err( `File already exists in the artifact registry: ${this.artifact.local_file_path}` )
62
- return false
73
+
74
+ const files = await listGenericArtifactFiles( this.artifact )
75
+
76
+ const newRemoteFilePath = parseGenericArtifactSchemaToRemoteRelativePath( this.artifact )
77
+
78
+ for ( const file of files ) {
79
+ const existingRemoteFilePath = parseGenericArtifactFileToRemoteRelativePath( file )
80
+ if ( existingRemoteFilePath === newRemoteFilePath ) {
81
+ err( `File already exists in the artifact registry: ${existingRemoteFilePath}` )
82
+ return false
83
+ }
63
84
  }
64
- command += ` --source=${this.artifact.local_file_path}`
85
+ const localPath = path.join( this.path, this.artifact.local_file_path )
86
+ command += ` --source=${localPath}`
65
87
  }
66
88
 
67
- if ( this.artifact.remote_path ) {
68
- command += ` --destination-path=${this.artifact.remote_path}`
69
- }
89
+ command += ` --destination-path=${this.artifact.remote_path || '""'}`
70
90
 
71
91
  debug( { command }, '<==Upload Command' )
72
92
 
73
93
  await shellCmd( command )
74
-
94
+
95
+ if ( pathToDelete ) {
96
+ await fs.promises.rm( pathToDelete, { recursive : true } )
97
+ }
98
+
75
99
  log( `Successfully pushed artifact: ${this.artifact.name}` )
76
100
 
77
101
  return true
78
102
  } catch ( error ) {
103
+ debug( { error }, '<==Error Pushing Artifact' )
79
104
  err( `Error pushing artifact: ${this.artifact.name} ${error.message}` )
80
105
  return false
81
106
  }
@@ -1,37 +1,42 @@
1
- import { assign, object, string, array, optional, enums, union, refine } from 'superstruct'
1
+ import path from 'node:path'
2
+
3
+ import { assign, coerce, object, string, array, optional, enums, union, refine } from 'superstruct'
2
4
 
3
5
  const ArtifactTypeEnum = {
4
6
  GENERIC : 'generic',
5
7
  }
6
8
 
7
- // Superstruct schema for individual artifact configuration
9
+ // Structs with the Partial prefix come from the need to validate the schema with defaults and without defaults merged together.
10
+
8
11
  const PartialArtifactSchema = object( {
9
12
  name : string(),
10
13
  type : optional( enums( Object.values( ArtifactTypeEnum ) ) ),
11
- gcp_project_id : optional( string() ),
12
- gcp_location : optional( string() ),
13
- gcp_artifact_registry_repository_name : optional( string() ),
14
- gcp_artifact_registry_package_name : optional( string() ),
15
- gcp_artifact_registry_package_version : optional( string() )
14
+ project : optional( string() ),
15
+ location : optional( string() ),
16
+ repository : optional( string() ),
17
+ package : optional( string() ),
18
+ version : optional( string() )
16
19
  } )
17
20
 
18
21
  const ArtifactSchema = assign( PartialArtifactSchema, object( {
19
22
  name : string(),
20
23
  type : enums( Object.values( ArtifactTypeEnum ) ),
21
- gcp_project_id : string(),
22
- gcp_location : string(),
23
- gcp_artifact_registry_repository_name : string(),
24
- gcp_artifact_registry_package_name : string(),
25
- gcp_artifact_registry_package_version : string()
24
+ project : string(),
25
+ location : string(),
26
+ repository : string(),
27
+ package : string(),
28
+ version : string()
26
29
  } ) )
27
30
 
31
+ const normalizedPath = () => coerce( string(), string(), value => path.normalize( value ) )
32
+
28
33
  const PartialGenericArtifactSchema = refine(
29
34
  assign( PartialArtifactSchema, object( {
30
- type : enums( [ ArtifactTypeEnum.GENERIC ] ),
31
- local_file_path : optional( string() ),
32
- local_path : optional( string() ),
33
- remote_file_path : optional( string() ),
34
- remote_path : optional( string() )
35
+ type : optional( enums( [ ArtifactTypeEnum.GENERIC ] ) ),
36
+ local_file_path : optional( normalizedPath() ),
37
+ local_path : optional( normalizedPath() ),
38
+ remote_file_path : optional( normalizedPath() ),
39
+ remote_path : optional( normalizedPath() )
35
40
  } ) ),
36
41
  'ConflictingPaths',
37
42
  ( value ) => {
@@ -47,10 +52,11 @@ const PartialGenericArtifactSchema = refine(
47
52
 
48
53
  const GenericArtifactSchema = refine(
49
54
  assign( ArtifactSchema, object( {
50
- local_file_path : optional( string() ),
51
- local_path : optional( string() ),
55
+ type : enums( [ ArtifactTypeEnum.GENERIC ] ),
56
+ local_file_path : optional( normalizedPath() ),
57
+ local_path : optional( normalizedPath() ),
52
58
  remote_file_path : optional( string() ),
53
- remote_path : optional( string() )
59
+ remote_path : optional( normalizedPath() )
54
60
  } ) ),
55
61
  'ConflictingPaths',
56
62
  ( value ) => {
@@ -66,11 +72,12 @@ const GenericArtifactSchema = refine(
66
72
 
67
73
  // Superstruct schema for defaults configuration
68
74
  const DefaultsSchema = object( {
69
- gcp_project_id : optional( string() ),
70
- gcp_location : optional( string() ),
71
- gcp_artifact_registry_repository_name : optional( string() ),
72
- gcp_artifact_registry_package_name : optional( string() ),
73
- gcp_artifact_registry_package_version : optional( string() )
75
+ type : optional( enums( Object.values( ArtifactTypeEnum ) ) ),
76
+ project : optional( string() ),
77
+ location : optional( string() ),
78
+ repository : optional( string() ),
79
+ package : optional( string() ),
80
+ version : optional( string() )
74
81
  } )
75
82
 
76
83
  // Superstruct schema for the complete artifacts configuration with defaults
@@ -1,16 +1,19 @@
1
1
  import fs from 'node:fs'
2
+ import path from 'node:path'
2
3
  import crypto from 'crypto'
3
4
 
4
5
  import { validate } from 'superstruct'
5
6
 
6
- import { parseYamlFile, warning } from '../Utils.mjs'
7
+ import { parseYamlFile, err, warning, debug, shellCmd } from '../Utils.mjs'
7
8
 
8
9
  import { ArtifactTypeEnum, ArtifactsSchema, GenericArtifactSchema } from './Structs.mjs'
9
10
 
10
- const getArtifactsSchema = async ( artifactsFilePath = './artifacts.yml' ) => {
11
+ const getArtifactsSchema = async ( artifactsFilePath = './artifacts.yaml' ) => {
11
12
  const artifactsConfig = await parseYamlFile( artifactsFilePath )
12
13
  const [ error, result ] = validate( artifactsConfig, ArtifactsSchema )
13
14
  if ( error ) {
15
+ err( `Invalid artifacts.yml file: ${error.message}` )
16
+ debug( { err : error }, '<==Invalid artifacts.yml file' )
14
17
  throw new Error( `Invalid artifacts.yml file: ${error.message}` )
15
18
  }
16
19
  return result
@@ -22,7 +25,7 @@ const flattenArtifactsSchema = ( artifactsSchema ) => {
22
25
  ...artifactsSchema.defaults,
23
26
  ...artifact
24
27
  }
25
-
28
+
26
29
  let schema
27
30
  switch ( mergedArtifact.type ) {
28
31
  case ArtifactTypeEnum.GENERIC:
@@ -49,44 +52,230 @@ const flattenArtifactsSchema = ( artifactsSchema ) => {
49
52
  }, [] )
50
53
  }
51
54
 
52
- const toImplicitPath = ( path ) => {
53
- let implicitPath = path
54
- // Remove leading './' if present
55
- if ( implicitPath.startsWith( './' ) ) {
56
- implicitPath = implicitPath.slice( 2 )
55
+ // Example of a file in a generic artifact registry repository:
56
+ // [
57
+ // {
58
+ // "createTime": "2025-12-02T17:52:08.694541Z",
59
+ // "hashes": [
60
+ // {
61
+ // "type": "SHA256",
62
+ // "value": "wrBRqJOUjm-zy6B7YzgKlK0fv9Ad0EEjydBnsmc9Ruw="
63
+ // },
64
+ // {
65
+ // "type": "MD5",
66
+ // "value": "Ebp3fVphzrITJ461Mg7Kmg=="
67
+ // }
68
+ // ],
69
+ // "name": "projects/leverege-registry/locations/us-central1/repositories/test-generic-registry/files/sweet-poems-1-1764697925426:v1:test1.txt%2Ftest1.txt",
70
+ // "owner": "projects/leverege-registry/locations/us-central1/repositories/test-generic-registry/packages/sweet-poems-1-1764697925426/versions/v1",
71
+ // "sizeBytes": "40",
72
+ // "updateTime": "2025-12-02T17:52:08.694541Z"
73
+ // }
74
+ // ]
75
+ // Obtained with:gcloud artifacts files list --project=leverege-registry --location=us-central1 --repository=test-generic-registry --package=sweet-poems-1-1764697925426 --version v1 --format="json"
76
+
77
+ const parseGenericArtifactFileToRemoteRelativePath = ( genericArtifactFile ) => {
78
+ // "projects/leverege-registry/locations/us-central1/repositories/test-generic-registry/files/sweet-poems-1-1764697925426:v1:test1.txt%2Ftest1.txt"
79
+ const artifactFileName = genericArtifactFile.name.split( '/' ).pop()
80
+ // sweet-poems-1-1764697925426:v1:test1.txt%2Ftest1.txt
81
+ const remoteRelativePath = artifactFileName.split( ':' ).pop().replaceAll( '%2F', '/' )
82
+ // test1.txt
83
+ return path.normalize( remoteRelativePath )
84
+ }
85
+
86
+ const parseGenericArtifactSchemaToRemoteRelativePath = ( genericArtifactSchema ) => {
87
+ if ( genericArtifactSchema.remote_file_path ) {
88
+ return path.normalize( genericArtifactSchema.remote_file_path )
89
+ }
90
+ if ( genericArtifactSchema.remote_path && genericArtifactSchema.local_file_path ) {
91
+ return path.join( genericArtifactSchema.remote_path, path.basename( genericArtifactSchema.local_file_path ) )
92
+ }
93
+ if ( !genericArtifactSchema.remote_file_path && !genericArtifactSchema.remote_path && genericArtifactSchema.local_file_path ) {
94
+ return path.basename( genericArtifactSchema.local_file_path )
57
95
  }
58
- // Remove trailing slash
59
- implicitPath = implicitPath.replace( /\/+$/, '' )
60
- return implicitPath
96
+ if ( !genericArtifactSchema.remote_file_path && !genericArtifactSchema.remote_path && genericArtifactSchema.local_file_path ) {
97
+ return path.basename( genericArtifactSchema.local_file_path )
98
+ }
99
+ if ( genericArtifactSchema.remote_path && genericArtifactSchema.local_path ) {
100
+ return path.normalize( genericArtifactSchema.remote_path )
101
+ }
102
+ return path.normalize( '.' )
61
103
  }
62
104
 
63
- const toExplicitPath = ( path ) => {
64
- let explicitPath = path
65
- // Keep absolute paths and paths already starting with '.' as-is
66
- if ( !path.startsWith( '/' ) && !path.startsWith( '.' ) ) {
67
- // Add './' to relative paths
68
- explicitPath = `./${explicitPath}`
105
+ const parseGenericArtifactSchemaToLocalRelativePath = ( genericArtifactSchema ) => {
106
+ if ( genericArtifactSchema.local_file_path ) {
107
+ return path.normalize( genericArtifactSchema.local_file_path )
108
+ }
109
+ if ( genericArtifactSchema.local_path && genericArtifactSchema.remote_file_path ) {
110
+ return path.join( genericArtifactSchema.local_path, path.basename( genericArtifactSchema.remote_file_path ) )
69
111
  }
70
- return explicitPath
112
+ if ( genericArtifactSchema.local_path && !genericArtifactSchema.remote_file_path && !genericArtifactSchema.remote_file_path ) {
113
+ return path.normalize( genericArtifactSchema.local_path )
114
+ }
115
+ if ( !genericArtifactSchema.local_file_path && !genericArtifactSchema.local_path && genericArtifactSchema.remote_file_path ) {
116
+ return path.basename( genericArtifactSchema.remote_file_path )
117
+ }
118
+ return path.normalize( '.' )
71
119
  }
72
120
 
73
- const deduplicatePath = ( path ) => {
74
- return path.replaceAll( '//', '/' )
121
+ const parseGenericArtifactSchemaToArtifactNameBase = ( genericArtifactSchema ) => {
122
+ const elements = [
123
+ 'projecs',
124
+ genericArtifactSchema.project,
125
+ 'locations',
126
+ genericArtifactSchema.location,
127
+ 'repositories',
128
+ genericArtifactSchema.repository,
129
+ 'files',
130
+ ]
131
+ return elements.join( '/' )
75
132
  }
76
133
 
77
- const convertGenericArtifactNameToPath = ( genericArtifactName ) => {
78
- return genericArtifactName.split( ':' ).pop().replaceAll( '%2F', '/' ).replaceAll( '%3A', ':' )
134
+ const parseGenericArtifactSchemaToArtifactNameFile = ( genericArtifactSchema ) => {
135
+ // // "name": "projects/leverege-registry/locations/us-central1/repositories/test-generic-registry/files/sweet-poems-1-1764697925426:v1:test1.txt%2Ftest1.txt",
136
+
137
+ const remoteRelativePath = parseGenericArtifactSchemaToRemoteRelativePath( genericArtifactSchema )
138
+ const encodedRemoteRelativePath = encodeURIComponent( remoteRelativePath )
139
+
140
+ const elements = [
141
+ genericArtifactSchema.package,
142
+ genericArtifactSchema.version,
143
+ encodedRemoteRelativePath,
144
+ ]
145
+ return elements.join( ':' )
146
+ }
147
+
148
+ const parseGenericArtifactSchemaToArtifactName = ( genericArtifactSchema ) => {
149
+ const base = parseGenericArtifactSchemaToArtifactNameBase( genericArtifactSchema )
150
+ const file = parseGenericArtifactSchemaToArtifactNameFile( genericArtifactSchema )
151
+
152
+ const elements = [
153
+ base,
154
+ file,
155
+ ]
156
+
157
+ return elements.join( ':' )
79
158
  }
80
159
 
81
- const calculateHash = ( path ) => {
160
+ const calculateHash = async ( path ) => {
82
161
  try {
83
162
  const hash = crypto.createHash( 'sha256' )
84
- hash.update( fs.readFileSync( path ) )
85
- return hash.digest( 'base64url' )
163
+ hash.update( await fs.promises.readFile( path ) )
164
+ return `${hash.digest( 'base64url' )}=`
86
165
  } catch ( error ) {
87
- console.error( error )
88
166
  throw new Error( `Failed to calculate hash for artifact ${path}: ${error.message}` )
89
167
  }
90
168
  }
91
169
 
92
- export { calculateHash, getArtifactsSchema, flattenArtifactsSchema, toImplicitPath, toExplicitPath, deduplicatePath, convertGenericArtifactNameToPath }
170
+ const listGenericArtifactFiles = async ( artifact ) => {
171
+ const command = `gcloud artifacts files list \
172
+ --project=${artifact.project} \
173
+ --location=${artifact.location} \
174
+ --repository=${artifact.repository} \
175
+ --package=${artifact.package} \
176
+ --version ${artifact.version} \
177
+ --format="json"`
178
+
179
+ debug( { command }, '<==List Files Command' )
180
+
181
+ const result = await shellCmd( command )
182
+
183
+ // Parse the JSON response
184
+ const files = JSON.parse( result )
185
+ return files
186
+ }
187
+
188
+ const getMissingRemoteFiles = async ( genericArtifactFiles, genericArtifactSchemaPath, genericArtifactSchema ) => {
189
+
190
+ const expectedRemoteRelativePath = parseGenericArtifactSchemaToRemoteRelativePath( genericArtifactSchema )
191
+ const localRelativePath = parseGenericArtifactSchemaToLocalRelativePath( genericArtifactSchema )
192
+ const expectedLocalRelativePath = path.join( genericArtifactSchemaPath, localRelativePath )
193
+
194
+ const actualFiles = new Set()
195
+ const expectedFiles = new Set()
196
+
197
+ for ( const file of genericArtifactFiles ) {
198
+ const actualRemoteFilePath = parseGenericArtifactFileToRemoteRelativePath( file )
199
+ const actualRemoteFileHash = file.hashes.find( hash => hash.type === 'SHA256' ).value
200
+ actualFiles.add( { remoteFilePath : actualRemoteFilePath, remoteFileHash : actualRemoteFileHash } )
201
+ }
202
+
203
+ if ( ( await fs.promises.stat( expectedLocalRelativePath ) ).isDirectory() ) {
204
+ const localFiles = await fs.promises.readdir( expectedLocalRelativePath, { withFileTypes : true, recursive : true } )
205
+ for ( const localFile of localFiles ) {
206
+ if ( localFile.isDirectory() ) {
207
+ continue
208
+ }
209
+
210
+ const expectedRemoteFilePath = path.join( expectedRemoteRelativePath, localFile.path.replace( expectedLocalRelativePath, '' ), localFile.name )
211
+ const expectedLocalFilePath = path.join( localFile.path, localFile.name )
212
+ expectedFiles.add( { remoteFilePath : expectedRemoteFilePath, remoteFileHash : await calculateHash( expectedLocalFilePath ) } )
213
+ }
214
+ } else {
215
+ const expectedRemoteFilePath = expectedRemoteRelativePath
216
+ const expectedLocalFilePath = expectedLocalRelativePath
217
+ expectedFiles.add( { remoteFilePath : expectedRemoteFilePath, remoteFileHash : await calculateHash( expectedLocalFilePath ) } )
218
+ }
219
+
220
+ const actualArray = Array.from( actualFiles )
221
+ const expectedArray = Array.from( expectedFiles )
222
+
223
+ return expectedArray.filter( expected => !actualArray.some( actual => actual.remoteFilePath === expected.remoteFilePath && actual.remoteFileHash === expected.remoteFileHash ) )
224
+ }
225
+
226
+ const shouldUseIgnoreConfiguration = async () => {
227
+ let result
228
+ try {
229
+ result = await shellCmd( 'gcloud config get-value gcloudignore/enabled' )
230
+ if ( result.trim() === '' || result.includes( 'has no property' ) ) {
231
+ return false
232
+ }
233
+ return result.trim() === 'true'
234
+ } catch ( error ) {
235
+ if ( error.message && error.message.includes( 'Section [gcloudignore] has no property [enabled]' ) ) {
236
+ return false
237
+ }
238
+ // Re-throw other errors
239
+ throw error
240
+ }
241
+ }
242
+
243
+ const findIgnoreFilePath = async ( relativePath ) => {
244
+ if ( relativePath ) {
245
+ const ignoreFilePath = path.join( relativePath, '.gcloudignore' )
246
+ if ( fs.existsSync( ignoreFilePath ) ) {
247
+ return ignoreFilePath
248
+ }
249
+ }
250
+
251
+ const ignoreFilePath = path.join( process.cwd(), '.gcloudignore' )
252
+ if ( fs.existsSync( ignoreFilePath ) ) {
253
+ return ignoreFilePath
254
+ }
255
+
256
+ return null
257
+ }
258
+
259
+ const copyFilesWithIgnoreConfiguration = async ( sourcePath, destinationPath, ignoreFilePath ) => {
260
+ // Ensure sourcePath ends with / so rsync copies contents (not the folder itself) and exclude patterns work correctly
261
+ const normalizedSourcePath = sourcePath.endsWith( '/' ) ? sourcePath : `${sourcePath}/`
262
+ const command = `rsync -av --delete --exclude-from='${ignoreFilePath}' ${normalizedSourcePath} ${destinationPath}`
263
+ await shellCmd( command )
264
+ }
265
+
266
+ export {
267
+ calculateHash,
268
+ getArtifactsSchema,
269
+ flattenArtifactsSchema,
270
+ parseGenericArtifactFileToRemoteRelativePath,
271
+ parseGenericArtifactSchemaToRemoteRelativePath,
272
+ parseGenericArtifactSchemaToLocalRelativePath,
273
+ parseGenericArtifactSchemaToArtifactNameBase,
274
+ parseGenericArtifactSchemaToArtifactNameFile,
275
+ parseGenericArtifactSchemaToArtifactName,
276
+ getMissingRemoteFiles,
277
+ listGenericArtifactFiles,
278
+ shouldUseIgnoreConfiguration,
279
+ findIgnoreFilePath,
280
+ copyFilesWithIgnoreConfiguration
281
+ }
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import path from 'node:path'
4
+
3
5
  import { program } from 'commander'
4
6
 
5
7
  import { warning, log } from '../Utils.mjs'
@@ -11,33 +13,36 @@ import { GenericArtifactPuller } from './ArtifactPuller.mjs'
11
13
  program
12
14
  .name( 'pull-artifact' )
13
15
  .description( 'Pull artifacts from GCP Artifact Registry' )
14
- .requiredOption( '-f, --file-path <path>', 'Path to artifacts.yaml file' )
15
- .option( '-n, --package-name <name>', 'Filter by artifact package name' )
16
- .option( '-v, --package-version <version>', 'Filter by artifact package version' )
16
+ .option( '-f, --file-path <file-path>', 'Path to artifacts.yaml file', './artifacts.yaml' )
17
+ .option( '-a, --artifact-path <artifact-path>', 'GCP artifact path' )
17
18
  .parse()
18
19
 
19
20
  const options = program.opts()
20
21
 
21
22
  async function main() {
22
- const artifactsYmlPath = options.filePath
23
- const artifactPackageName = options.packageName || null
24
- const artifactPackageVersion = options.packageVersion || null
25
-
26
- const artifactsSchema = await getArtifactsSchema( artifactsYmlPath )
23
+ const artifactsSchema = await getArtifactsSchema( options.filePath )
27
24
  const flattenedArtifactsSchema = flattenArtifactsSchema( artifactsSchema )
28
-
29
- let filteredArtifactsSchema
30
- if ( artifactPackageName && artifactPackageVersion ) {
31
- filteredArtifactsSchema = flattenedArtifactsSchema.filter(
32
- artifact => artifact.gcp_artifact_registry_package_name === artifactPackageName &&
33
- artifact.gcp_artifact_registry_package_version === artifactPackageVersion )
34
- } else if ( artifactPackageName ) {
25
+
26
+ let filteredArtifactsSchema = flattenedArtifactsSchema
27
+
28
+ if ( options.artifactPath ) {
29
+ const artifactPathElements = options.artifactPath.split( '/' )
30
+
31
+ if ( artifactPathElements.length !== 5 ) {
32
+ warning( 'Invalid artifact path, it must be in the format <project>/<location>/<repository>/<package>/<version>' )
33
+ return false
34
+ }
35
+
36
+ const [ project, location, repository, packageName, version ] = artifactPathElements
37
+
35
38
  filteredArtifactsSchema = flattenedArtifactsSchema.filter(
36
- artifact => artifact.gcp_artifact_registry_package_name === artifactPackageName )
37
- } else {
38
- filteredArtifactsSchema = flattenedArtifactsSchema
39
+ artifact => artifact.project === project &&
40
+ artifact.location === location &&
41
+ artifact.repository === repository &&
42
+ artifact.package === packageName &&
43
+ artifact.version === version )
39
44
  }
40
-
45
+
41
46
  let counter = 0
42
47
 
43
48
  const pullResults = await Promise.all(
@@ -45,7 +50,7 @@ async function main() {
45
50
  let puller
46
51
  switch ( artifactSchema.type ) {
47
52
  case ArtifactTypeEnum.GENERIC:
48
- puller = new GenericArtifactPuller( artifactSchema )
53
+ puller = new GenericArtifactPuller( path.dirname( options.filePath ), artifactSchema )
49
54
  break
50
55
  default:
51
56
  warning( `Unsupported artifact type "${artifactSchema.type}"` )
@@ -69,6 +74,8 @@ async function main() {
69
74
  counter = pullResults.filter( Boolean ).length
70
75
 
71
76
  log( `Successfully pulled ${counter}/${filteredArtifactsSchema.length} artifacts` )
77
+
78
+ return counter === filteredArtifactsSchema.length
72
79
  }
73
80
 
74
81
  main()
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import path from 'node:path'
4
+
3
5
  import { program } from 'commander'
4
6
 
5
7
  import { warning, log } from '../Utils.mjs'
@@ -11,31 +13,33 @@ import { GenericArtifactPusher } from './ArtifactPusher.mjs'
11
13
  program
12
14
  .name( 'push-artifact' )
13
15
  .description( 'Push artifacts to GCP Artifact Registry' )
14
- .requiredOption( '-f, --file-path <path>', 'Path to artifacts.yaml file' )
15
- .option( '-n, --package-name <name>', 'Filter by artifact package name' )
16
- .option( '-v, --package-version <version>', 'Filter by artifact package version' )
16
+ .option( '-f, --file-path <file-path>', 'Path to artifacts.yaml file', './artifacts.yaml' )
17
+ .option( '-a, --artifact-path <artifact-path>', 'GCP artifact path' )
17
18
  .parse()
18
19
 
19
20
  const options = program.opts()
20
21
 
21
22
  async function main() {
22
- const artifactsYmlPath = options.filePath
23
- const artifactPackageName = options.packageName || null
24
- const artifactPackageVersion = options.packageVersion || null
25
-
26
- const artifactsSchema = await getArtifactsSchema( artifactsYmlPath )
23
+ const artifactsSchema = await getArtifactsSchema( options.filePath )
27
24
  const flattenedArtifactsSchema = flattenArtifactsSchema( artifactsSchema )
28
-
29
- let filteredArtifactsSchema
30
- if ( artifactPackageName && artifactPackageVersion ) {
31
- filteredArtifactsSchema = flattenedArtifactsSchema.filter(
32
- artifact => artifact.gcp_artifact_registry_package_name === artifactPackageName &&
33
- artifact.gcp_artifact_registry_package_version === artifactPackageVersion )
34
- } else if ( artifactPackageName ) {
25
+
26
+ let filteredArtifactsSchema = flattenedArtifactsSchema
27
+
28
+ if ( options.artifactPath ) {
29
+ const artifactPathElements = options.artifactPath.split( '/' )
30
+ if ( artifactPathElements.length !== 5 ) {
31
+ warning( 'Invalid artifact path, it must be in the format <project>/<location>/<repository>/<package>/<version>' )
32
+ return false
33
+ }
34
+
35
+ const [ project, location, repository, packageName, version ] = artifactPathElements
36
+
35
37
  filteredArtifactsSchema = flattenedArtifactsSchema.filter(
36
- artifact => artifact.gcp_artifact_registry_package_name === artifactPackageName )
37
- } else {
38
- filteredArtifactsSchema = flattenedArtifactsSchema
38
+ artifact => artifact.project === project &&
39
+ artifact.location === location &&
40
+ artifact.repository === repository &&
41
+ artifact.package === packageName &&
42
+ artifact.version === version )
39
43
  }
40
44
 
41
45
  let counter = 0
@@ -45,7 +49,7 @@ async function main() {
45
49
  let pusher
46
50
  switch ( artifactSchema.type ) {
47
51
  case ArtifactTypeEnum.GENERIC:
48
- pusher = new GenericArtifactPusher( artifactSchema )
52
+ pusher = new GenericArtifactPusher( path.dirname( options.filePath ), artifactSchema )
49
53
  break
50
54
  default:
51
55
  warning( `Unsupported artifact type "${artifactSchema.type}"` )
@@ -69,6 +73,8 @@ async function main() {
69
73
  counter = pushResults.filter( Boolean ).length
70
74
 
71
75
  log( `Successfully pushed ${counter}/${filteredArtifactsSchema.length} artifacts` )
76
+
77
+ return counter === filteredArtifactsSchema.length
72
78
  }
73
79
 
74
80
  main()
package/src/bash-funcs CHANGED
@@ -9,7 +9,11 @@
9
9
  # see exactly what certain commands are complaining about. Running any of the
10
10
  # build-tools with BUILD_TOOLS_DEBUG=1 will redirect the noise to stdout.
11
11
  DEVNULL="/dev/null"
12
- [ ! -z "$BUILD_TOOLS_DEBUG" ] && DEVNULL="/dev/stdout" && printf "\n***BUILD_TOOLS_DEBUG DEVNULL=>$DEVNULL\n"
12
+ if [[ -n "${BUILD_TOOLS_DEBUG:-}" ]];
13
+ then
14
+ DEVNULL="/dev/stdout"
15
+ printf "\n***BUILD_TOOLS_DEBUG DEVNULL=>$DEVNULL\n"
16
+ fi
13
17
 
14
18
  GITTOP=`git rev-parse --show-toplevel`
15
19
  DTS="`date +\"%Y%m%d-%H%M%S\"`"
@@ -305,6 +309,11 @@ PLUGIN_NOEXEC
305
309
  return 0
306
310
  }
307
311
 
312
+ # simple check to allow us to avoid double settings the k8s context
313
+ function fromHelmup() {
314
+ [[ "${_FROM_HELMUP_-false}" == "true" ]]
315
+ }
316
+
308
317
  # simple enforcement of necessary tools that need to be installed
309
318
  function commandChecker() {
310
319
  for cmd in "$@"; do
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ version="${1:-X.Y.Z}"
5
+ date_str=$(date +%F) # e.g., 2025-12-11
6
+
7
+ cat <<EOF
8
+ ## [$version] - $date_str
9
+
10
+ ### Added
11
+ -
12
+
13
+ ### Changed
14
+ -
15
+
16
+ ### Deprecated
17
+ -
18
+
19
+ ### Removed
20
+ -
21
+
22
+ ### Fixed
23
+ -
24
+
25
+ ### Migration Notes
26
+ -
27
+ EOF
@@ -79,7 +79,7 @@ if ( args._unknown ) {
79
79
 
80
80
  const minNodejsVersion = '22.13.0'
81
81
  if ( semverLt( process.version, minNodejsVersion ) ) {
82
- warning( `suggest upgrading to at least node ${minNodejsVersion} - xxx\n` )
82
+ warning( `suggest upgrading to at least node ${minNodejsVersion}\n` )
83
83
  }
84
84
 
85
85
  // First of all, fail if we are not in a git repository
@@ -116,20 +116,33 @@ const buildCompassMap = ( yamlFile ) => {
116
116
  return chartCompass
117
117
  }
118
118
 
119
- const findChartUrl = ( config, chartName ) => {
120
- if ( !chartName ) {
121
- errorExit( 'no chart name speficied' )
122
- }
119
+ const findChartUrl = ( config, serviceName ) => {
120
+ if ( !serviceName ) errorExit( 'no chart name specified' )
123
121
 
124
122
  for ( const entry of config ) {
125
123
  const chartsPath = entry.charts_path || 'charts'
126
- for ( const [ repo, charts ] of Object.entries( entry.repositories ) ) {
127
- debug( { repo, charts }, `<==findChartUrl in repo ${chalk.bold.yellow( repo )}` )
128
- if ( charts.includes( chartName ) ) {
129
- return `${entry.registry}/${repo}/${chartsPath}/${chartName}`
124
+ const repositories = entry.repositories || {}
125
+
126
+ for ( const [ repo, chartMap ] of Object.entries( repositories ) ) {
127
+ debug( { repo, chartMap }, `<== findChartUrl in repo ${repo}` )
128
+
129
+ for ( const [ chartKey, cfg ] of Object.entries( chartMap ) ) {
130
+ const chartCfg = cfg || {}
131
+ const chartArtifact = chartCfg.chart || chartKey
132
+ const aliases = chartCfg.aliases || []
133
+
134
+ const matches =
135
+ serviceName === chartKey ||
136
+ serviceName === chartArtifact ||
137
+ aliases.includes( serviceName )
138
+
139
+ if ( matches ) {
140
+ return `${entry.registry}/${repo}/${chartsPath}/${chartArtifact}`
141
+ }
130
142
  }
131
143
  }
132
144
  }
145
+
133
146
  return undefined
134
147
  }
135
148
 
@@ -1,22 +1,27 @@
1
1
  - registry: us-docker.pkg.dev/leverege-registry
2
+ charts_path: charts
2
3
  repositories:
3
4
  stack:
4
- - api-server
5
- - authz-server
6
- - db-curator
7
- - emailer
8
- - imagine
9
- - message-processor
10
- - messenger
11
- - reason
12
- - resource-server
13
- - rule-enginer
14
- - scheduler
15
- - transponder-bq
16
- - transponder-dh
17
- - transponder-rt
18
- - transponder-tsdb
5
+ api-server: {}
6
+ authz-server: {}
7
+ db-curator: {}
8
+ emailer: {}
9
+ imagine: {}
10
+ message-processor: {}
11
+ messenger: {}
12
+ reason: {}
13
+ resource-server: {}
14
+ rule-enginer: {}
15
+ scheduler: {}
16
+
17
+ transponder:
18
+ aliases:
19
+ - transponder-bq
20
+ - transponder-dh
21
+ - transponder-rt
22
+ - transponder-tsdb
23
+
19
24
  leverege:
20
- - pubsub-pulse
21
- - pusher
22
- - overdose
25
+ pubsub-pulse: {}
26
+ pusher: {}
27
+ overdose: {}
@@ -117,7 +117,7 @@ if ( requiresHelmChart ) {
117
117
  }
118
118
 
119
119
  const leveregeStanza = isNodeJsProject ? closestPackageJson.leverege : repoDescr.rootPyProjectToml.leverege
120
- const runCommand = isYarnProject ? 'node entrypoint.cjs' : 'node index.js'
120
+ const runCommand = isYarnProject ? 'node entrypoint.js' : 'node index.js'
121
121
 
122
122
  const dockerfileOptions = {
123
123
  isNodeJsProject,
@@ -1,30 +1,30 @@
1
1
  #!/bin/bash
2
2
  #
3
3
  showInstalling "The Prometheus Operator and Components"
4
- addHelmRepo prometheus-community https://prometheus-community.github.io/helm-charts
5
4
 
6
5
  NS="--namespace prometheus"
7
6
 
8
7
  showInstalling "The Prometheus Operator (kube-prometheus-stack)"
9
- [ -z "$PROMETHEUS_STACK_CHART_VERSION" ] && PROMETHEUS_STACK_CHART_VERSION="79"
10
8
 
11
9
  # https://artifacthub.io/packages/helm/prometheus-community/kube-prometheus-stack
12
- helm upgrade $NS --install prometheus-stack prometheus-community/kube-prometheus-stack \
10
+ OCI_CHART="oci://ghcr.io/prometheus-community/charts/kube-prometheus-stack"
11
+ [ -z "$PROMETHEUS_STACK_CHART_VERSION" ] && PROMETHEUS_STACK_CHART_VERSION="80.4.1"
12
+ helm upgrade $NS --install prometheus-stack $OCI_CHART \
13
13
  --values prom-operator/prometheus-stack.yaml \
14
14
  --version $PROMETHEUS_STACK_CHART_VERSION $HELM_WHAT
15
15
 
16
16
  # https://artifacthub.io/packages/helm/prometheus-community/prometheus-elasticsearch-exporter
17
17
  showInstalling "The Elasticsearch Exporter (prom-operator ES metrics exports)"
18
- [ -z "$ELASTICSEARCH_EXPORTER_CHART_VERSION" ] && ELASTICSEARCH_EXPORTER_CHART_VERSION="6"
19
- helm upgrade $NS --install elasticsearch8-exporter prometheus-community/prometheus-elasticsearch-exporter \
18
+ [ -z "$ELASTICSEARCH_EXPORTER_CHART_VERSION" ] && ELASTICSEARCH_EXPORTER_CHART_VERSION="7.2.1"
19
+ OCI_CHART="oci://ghcr.io/prometheus-community/charts/prometheus-elasticsearch-exporter"
20
+ helm upgrade $NS --install elasticsearch8-exporter $OCI_CHART \
20
21
  --values prom-operator/elasticsearch-exporter.yaml \
21
22
  --version $ELASTICSEARCH_EXPORTER_CHART_VERSION $HELM_WHAT
22
23
 
23
24
  # https://artifacthub.io/packages/helm/prometheus-community/prometheus-stackdriver-exporter
24
25
  showInstalling "The Stackdriver Exporter (prom-operator GKE metrics exports)"
25
- [ -z "$STACKDRIVER_EXPORTER_CHART_VERSION" ] && STACKDRIVER_EXPORTER_CHART_VERSION="4"
26
- helm upgrade $NS --install stackdriver-exporter prometheus-community/prometheus-stackdriver-exporter \
26
+ [ -z "$STACKDRIVER_EXPORTER_CHART_VERSION" ] && STACKDRIVER_EXPORTER_CHART_VERSION="4.12.2"
27
+ OCI_CHART="oci://ghcr.io/prometheus-community/charts/prometheus-stackdriver-exporter"
28
+ helm upgrade $NS --install stackdriver-exporter $OCI_CHART \
27
29
  --values prom-operator/stackdriver-exporter.yaml \
28
30
  --version $STACKDRIVER_EXPORTER_CHART_VERSION $HELM_WHAT
29
-
30
- removeHelmRepo prometheus-community
package/src/helmup.sh CHANGED
@@ -2,6 +2,9 @@
2
2
  #
3
3
  # printf "\n***** RUNNING LOCALLY *****\n" && sleep 2 && alias build-tools="build-tools.js"
4
4
 
5
+ # indicator to the bash-funcs ifNotFromHelmup function
6
+ export _FROM_HELMUP_="true"
7
+
5
8
  # Load the standard helper functions
6
9
  . `build-tools --bashfun`
7
10
 
package/src/k8scale.sh CHANGED
@@ -29,8 +29,12 @@ K8SCALE_HELP
29
29
  exit 1
30
30
  esac
31
31
 
32
- overwhelm
33
- [ $? -ne 0 ] && printf "\n\n***Aborting helmup...\n\n" && exit 1
32
+ if fromHelmup;
33
+ then
34
+ printf "Skipping overwhelm because invoked from helmup\n"
35
+ else
36
+ overwhelm || { printf "\n\n***Aborting helmup...\n\n"; exit 1; }
37
+ fi
34
38
 
35
39
  printf " scaled => `color g $DIRECTION`\n\n"
36
40
  for DEPLOYMENT in "$@";
@@ -0,0 +1,60 @@
1
+ #! /usr/bin/env node
2
+ import { $, chalk, fs, path } from 'zx'
3
+ import findYarnWorkspaceRoot from 'find-yarn-workspace-root'
4
+
5
+ import { log } from './Utils.mjs'
6
+
7
+ const $$ = $( {
8
+ stdio : 'inherit',
9
+ quiet : true
10
+ } )
11
+
12
+ const generateEntrypointFile = main => `
13
+ import path from 'node:path'
14
+ import { fileURLToPath } from 'url'
15
+ import { createRequire } from 'node:module'
16
+
17
+ const require = createRequire( import.meta.url )
18
+ const __filename = fileURLToPath(import.meta.url)
19
+ const __dirname = path.dirname(__filename)
20
+
21
+ const indexPath = path.normalize(path.resolve( __dirname,'${main}'))
22
+ process.env.APP_PLUGIN_ROOT = path.dirname( indexPath )
23
+ const index = require(indexPath)
24
+
25
+ export default index
26
+ `
27
+
28
+ const packageJson = await fs.readJSON( path.join( process.env.INIT_CWD, './package.json' ) )
29
+ log( `Building ${packageJson.name}...` )
30
+ const mainEntry = path.join( process.env.INIT_CWD, packageJson.main )
31
+ const root = findYarnWorkspaceRoot( process.env.INIT_CWD ) || process.env.INIT_CWD
32
+
33
+ const main = path.relative( root, mainEntry )
34
+
35
+ const packageName = process.env.npm_package_name
36
+ const buildDir = `/tmp/${packageName.replace( '@leverege/', '' )}/build`
37
+ log( `Output directory: ${buildDir}` )
38
+
39
+ // await $$`yarn clean`
40
+ log( 'Cleaning previous build output...' )
41
+ await $$`rm -rf ${buildDir}`.catch( () => {} )
42
+ await $$`mkdir -p ${buildDir}`.catch( () => {} )
43
+ log( 'Ensuring yarn bundle plugin...' )
44
+ let plugins = $.sync`yarn plugin runtime --json`.stdout.trim()
45
+ plugins = plugins != null && plugins.length > 0 ? plugins.split( '\n' ).map( JSON.parse ) : []
46
+ if ( !plugins.some( plg => plg.name === '@yarn.build/plugin-bundle' ) ) {
47
+ log( chalk.redBright( 'Yarn bundle plugin missing!' ) )
48
+ log( `To fix this error, run:\n${chalk.green( 'yarn plugin import https://yarn.build/latest/bundle' )}` )
49
+ process.exit( 1 )
50
+ }
51
+ log( 'Installing focused yarn dependencies...' )
52
+ await $$`yarn workspaces focus ${packageName}`
53
+ log( 'Bundling...' )
54
+ await $$`yarn bundle --no-compress --output-directory ${buildDir}`
55
+ log( 'Cleaning up unnecessary output files...' )
56
+ await $$`rm -rf ${buildDir}/node_modules ${buildDir}/entrypoint.js`
57
+
58
+ const entrypointFile = generateEntrypointFile( main )
59
+
60
+ await fs.writeFile( `${buildDir}/entrypoint.js`, entrypointFile, 'utf-8' )