@leverege/build-tools 2.93.3 → 2.93.5

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.
@@ -0,0 +1,3 @@
1
+ {
2
+ "editor.formatOnSave": true
3
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leverege/build-tools",
3
- "version": "2.93.3",
3
+ "version": "2.93.5",
4
4
  "description": "A collection of build / support tools for Leverege developers",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -11,13 +11,15 @@
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",
21
23
  "chart-to-museum": "src/chart-to-museum.mjs",
22
24
  "chart-to-registry": "src/chart-to-registry.mjs",
23
25
  "circleate": "src/circleate.sh",
@@ -76,8 +78,8 @@
76
78
  "commander": "^14.0.2",
77
79
  "deepmerge": "^4.3.1",
78
80
  "enquirer": "^2.4.1",
79
- "execa": "^9.6.0",
80
- "glob": "^11.0.3",
81
+ "execa": "^9.6.1",
82
+ "glob": "^13.0.0",
81
83
  "handlebars": "^4.7.8",
82
84
  "ignore": "^7.0.5",
83
85
  "inquirer": "^12.11.1",
@@ -101,6 +103,6 @@
101
103
  "@leverege/eslint-config-leverege": "^5.1.1",
102
104
  "chai": "^6.2.1",
103
105
  "mocha": "^11.7.5",
104
- "npm": "^11.6.2"
106
+ "npm": "^11.7.0"
105
107
  }
106
- }
108
+ }
@@ -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,16 @@
1
+ import path from 'node:path'
2
+
1
3
  import { shellCmd, log, err, debug } from '../Utils.mjs'
2
4
 
3
- import { convertGenericArtifactNameToPath } from './Utils.mjs'
5
+ import {
6
+ parseGenericArtifactFileToRemoteRelativePath,
7
+ parseGenericArtifactSchemaToRemoteRelativePath,
8
+ listGenericArtifactFiles
9
+ } from './Utils.mjs'
4
10
 
5
11
  class ArtifactPusher {
6
- constructor( artifact ) {
12
+ constructor( path, artifact ) {
13
+ this.path = path
7
14
  this.artifact = artifact
8
15
  }
9
16
 
@@ -13,25 +20,6 @@ class ArtifactPusher {
13
20
  }
14
21
 
15
22
  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
23
 
36
24
  async push() {
37
25
  log( `Pushing artifact: ${this.artifact.name}` )
@@ -46,27 +34,34 @@ class GenericArtifactPusher extends ArtifactPusher {
46
34
  let command = ''
47
35
 
48
36
  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}`
37
+ --project=${this.artifact.project} \
38
+ --location=${this.artifact.location} \
39
+ --repository=${this.artifact.repository} \
40
+ --package=${this.artifact.package} \
41
+ --version=${this.artifact.version}`
54
42
 
55
43
  if ( this.artifact.local_path ) {
56
- command += ` --source-directory=${this.artifact.local_path}`
44
+ const localPath = path.join( this.path, this.artifact.local_path )
45
+ command += ` --source-directory=${localPath}`
57
46
  command += ' --skip-existing'
58
47
  } 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
48
+
49
+ const files = await listGenericArtifactFiles( this.artifact )
50
+
51
+ const newRemoteFilePath = parseGenericArtifactSchemaToRemoteRelativePath( this.artifact )
52
+
53
+ for ( const file of files ) {
54
+ const existingRemoteFilePath = parseGenericArtifactFileToRemoteRelativePath( file )
55
+ if ( existingRemoteFilePath === newRemoteFilePath ) {
56
+ err( `File already exists in the artifact registry: ${existingRemoteFilePath}` )
57
+ return false
58
+ }
63
59
  }
64
- command += ` --source=${this.artifact.local_file_path}`
60
+ const localPath = path.join( this.path, this.artifact.local_file_path )
61
+ command += ` --source=${localPath}`
65
62
  }
66
63
 
67
- if ( this.artifact.remote_path ) {
68
- command += ` --destination-path=${this.artifact.remote_path}`
69
- }
64
+ command += ` --destination-path=${this.artifact.remote_path || '""'}`
70
65
 
71
66
  debug( { command }, '<==Upload Command' )
72
67
 
@@ -76,6 +71,7 @@ class GenericArtifactPusher extends ArtifactPusher {
76
71
 
77
72
  return true
78
73
  } catch ( error ) {
74
+ debug( { error }, '<==Error Pushing Artifact' )
79
75
  err( `Error pushing artifact: ${this.artifact.name} ${error.message}` )
80
76
  return false
81
77
  }
@@ -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,9 +1,10 @@
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
 
@@ -11,6 +12,8 @@ const getArtifactsSchema = async ( artifactsFilePath = './artifacts.yml' ) => {
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,187 @@ 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 )
95
+ }
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 )
57
101
  }
58
- // Remove trailing slash
59
- implicitPath = implicitPath.replace( /\/+$/, '' )
60
- return implicitPath
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 )
69
108
  }
70
- return explicitPath
109
+ if ( genericArtifactSchema.local_path && genericArtifactSchema.remote_file_path ) {
110
+ return path.join( genericArtifactSchema.local_path, path.basename( genericArtifactSchema.remote_file_path ) )
111
+ }
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( '.' )
119
+ }
120
+
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( '/' )
71
132
  }
72
133
 
73
- const deduplicatePath = ( path ) => {
74
- return path.replaceAll( '//', '/' )
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( ':' )
75
146
  }
76
147
 
77
- const convertGenericArtifactNameToPath = ( genericArtifactName ) => {
78
- return genericArtifactName.split( ':' ).pop().replaceAll( '%2F', '/' ).replaceAll( '%3A', ':' )
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
166
  console.error( error )
88
167
  throw new Error( `Failed to calculate hash for artifact ${path}: ${error.message}` )
89
168
  }
90
169
  }
91
170
 
92
- export { calculateHash, getArtifactsSchema, flattenArtifactsSchema, toImplicitPath, toExplicitPath, deduplicatePath, convertGenericArtifactNameToPath }
171
+ const listGenericArtifactFiles = async ( artifact ) => {
172
+ const command = `gcloud artifacts files list \
173
+ --project=${artifact.project} \
174
+ --location=${artifact.location} \
175
+ --repository=${artifact.repository} \
176
+ --package=${artifact.package} \
177
+ --version ${artifact.version} \
178
+ --format="json"`
179
+
180
+ debug( { command }, '<==List Files Command' )
181
+
182
+ const result = await shellCmd( command )
183
+
184
+ // Parse the JSON response
185
+ const files = JSON.parse( result )
186
+ return files
187
+ }
188
+
189
+ const checkGenericArtifactMatch = async ( genericArtifactFiles, genericArtifactSchemaPath, genericArtifactSchema ) => {
190
+
191
+ const expectedRemoteRelativePath = parseGenericArtifactSchemaToRemoteRelativePath( genericArtifactSchema )
192
+ const localRelativePath = parseGenericArtifactSchemaToLocalRelativePath( genericArtifactSchema )
193
+ const expectedLocalRelativePath = path.join( genericArtifactSchemaPath, localRelativePath )
194
+
195
+ const actualFiles = new Set()
196
+ const expectedFiles = new Set()
197
+
198
+ for ( const file of genericArtifactFiles ) {
199
+ const actualRemoteFilePath = parseGenericArtifactFileToRemoteRelativePath( file )
200
+ const actualRemoteFileHash = file.hashes.find( hash => hash.type === 'SHA256' ).value
201
+ actualFiles.add( { remoteFilePath : actualRemoteFilePath, remoteFileHash : actualRemoteFileHash } )
202
+ }
203
+
204
+ if ( ( await fs.promises.stat( expectedLocalRelativePath ) ).isDirectory() ) {
205
+ const localFiles = await fs.promises.readdir( expectedLocalRelativePath, { withFileTypes : true, recursive : true } )
206
+ for ( const localFile of localFiles ) {
207
+ if ( localFile.isDirectory() ) {
208
+ continue
209
+ }
210
+
211
+ const expectedRemoteFilePath = path.join( expectedRemoteRelativePath, localFile.path.replace( expectedLocalRelativePath, '' ), localFile.name )
212
+ const expectedLocalFilePath = path.join( localFile.path, localFile.name )
213
+ expectedFiles.add( { remoteFilePath : expectedRemoteFilePath, remoteFileHash : await calculateHash( expectedLocalFilePath ) } )
214
+ }
215
+ } else {
216
+ const expectedRemoteFilePath = expectedRemoteRelativePath
217
+ const expectedLocalFilePath = expectedLocalRelativePath
218
+ expectedFiles.add( { remoteFilePath : expectedRemoteFilePath, remoteFileHash : await calculateHash( expectedLocalFilePath ) } )
219
+ }
220
+
221
+ const actualArray = Array.from( actualFiles )
222
+ const expectedArray = Array.from( expectedFiles )
223
+
224
+ return actualArray.length === expectedArray.length &&
225
+ expectedArray.every( expected => actualArray.some( actual => actual.remoteFilePath === expected.remoteFilePath && actual.remoteFileHash === expected.remoteFileHash ) )
226
+ }
227
+
228
+ export {
229
+ calculateHash,
230
+ getArtifactsSchema,
231
+ flattenArtifactsSchema,
232
+ parseGenericArtifactFileToRemoteRelativePath,
233
+ parseGenericArtifactSchemaToRemoteRelativePath,
234
+ parseGenericArtifactSchemaToLocalRelativePath,
235
+ parseGenericArtifactSchemaToArtifactName,
236
+ checkGenericArtifactMatch,
237
+ listGenericArtifactFiles
238
+ }
@@ -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'
@@ -12,32 +14,26 @@ program
12
14
  .name( 'pull-artifact' )
13
15
  .description( 'Pull artifacts from GCP Artifact Registry' )
14
16
  .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' )
17
+ .requiredOption( '-p, --project <project>', 'GCP project id' )
18
+ .requiredOption( '-l, --location <location>', 'GCP location' )
19
+ .requiredOption( '-r, --repository <repository>', 'GCP artifact registry repository' )
20
+ .requiredOption( '-n --package <name>', 'GCP artifact registry package name' )
21
+ .requiredOption( '-v, --version <version>', 'GCP artifact registry package version' )
17
22
  .parse()
18
23
 
19
24
  const options = program.opts()
20
25
 
21
26
  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 )
27
+ const artifactsSchema = await getArtifactsSchema( options.filePath )
27
28
  const flattenedArtifactsSchema = flattenArtifactsSchema( artifactsSchema )
28
29
 
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 ) {
35
- filteredArtifactsSchema = flattenedArtifactsSchema.filter(
36
- artifact => artifact.gcp_artifact_registry_package_name === artifactPackageName )
37
- } else {
38
- filteredArtifactsSchema = flattenedArtifactsSchema
39
- }
40
-
30
+ const filteredArtifactsSchema = flattenedArtifactsSchema.filter(
31
+ artifact => artifact.project === options.project &&
32
+ artifact.location === options.location &&
33
+ artifact.repository === options.repository &&
34
+ artifact.package === options.package &&
35
+ artifact.version === options.version )
36
+
41
37
  let counter = 0
42
38
 
43
39
  const pullResults = await Promise.all(
@@ -45,7 +41,7 @@ async function main() {
45
41
  let puller
46
42
  switch ( artifactSchema.type ) {
47
43
  case ArtifactTypeEnum.GENERIC:
48
- puller = new GenericArtifactPuller( artifactSchema )
44
+ puller = new GenericArtifactPuller( path.dirname( options.filePath ), artifactSchema )
49
45
  break
50
46
  default:
51
47
  warning( `Unsupported artifact type "${artifactSchema.type}"` )
@@ -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'
@@ -12,31 +14,25 @@ program
12
14
  .name( 'push-artifact' )
13
15
  .description( 'Push artifacts to GCP Artifact Registry' )
14
16
  .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' )
17
+ .requiredOption( '-p, --project <project>', 'GCP project id' )
18
+ .requiredOption( '-l, --location <location>', 'GCP location' )
19
+ .requiredOption( '-r, --repository <repository>', 'GCP artifact registry repository' )
20
+ .requiredOption( '-n --package <name>', 'GCP artifact registry package name' )
21
+ .requiredOption( '-v, --version <version>', 'GCP artifact registry package version' )
17
22
  .parse()
18
23
 
19
24
  const options = program.opts()
20
25
 
21
26
  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 )
27
+ const artifactsSchema = await getArtifactsSchema( options.filePath )
27
28
  const flattenedArtifactsSchema = flattenArtifactsSchema( artifactsSchema )
28
29
 
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 ) {
35
- filteredArtifactsSchema = flattenedArtifactsSchema.filter(
36
- artifact => artifact.gcp_artifact_registry_package_name === artifactPackageName )
37
- } else {
38
- filteredArtifactsSchema = flattenedArtifactsSchema
39
- }
30
+ const filteredArtifactsSchema = flattenedArtifactsSchema.filter(
31
+ artifact => artifact.project === options.project &&
32
+ artifact.location === options.location &&
33
+ artifact.repository === options.repository &&
34
+ artifact.package === options.package &&
35
+ artifact.version === options.version )
40
36
 
41
37
  let counter = 0
42
38
 
@@ -45,7 +41,7 @@ async function main() {
45
41
  let pusher
46
42
  switch ( artifactSchema.type ) {
47
43
  case ArtifactTypeEnum.GENERIC:
48
- pusher = new GenericArtifactPusher( artifactSchema )
44
+ pusher = new GenericArtifactPusher( path.dirname( options.filePath ), artifactSchema )
49
45
  break
50
46
  default:
51
47
  warning( `Unsupported artifact type "${artifactSchema.type}"` )
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\"`"
@@ -306,8 +310,8 @@ PLUGIN_NOEXEC
306
310
  }
307
311
 
308
312
  # simple check to allow us to avoid double settings the k8s context
309
- function ifNotFromHelmup() {
310
- [ "$_FROM_HELMUP_" != "true" ]
313
+ function fromHelmup() {
314
+ [[ "${_FROM_HELMUP_-false}" == "true" ]]
311
315
  }
312
316
 
313
317
  # simple enforcement of necessary tools that need to be installed
@@ -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
@@ -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.2.0"
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.0"
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/k8scale.sh CHANGED
@@ -29,9 +29,12 @@ K8SCALE_HELP
29
29
  exit 1
30
30
  esac
31
31
 
32
- [ ifNotFromHelmup ] && overwhelm
33
-
34
- [ $? -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
35
38
 
36
39
  printf " scaled => `color g $DIRECTION`\n\n"
37
40
  for DEPLOYMENT in "$@";