@leverege/build-tools 2.93.3-beta.6 → 2.93.3

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.93.3-beta.6",
3
+ "version": "2.93.3",
4
4
  "description": "A collection of build / support tools for Leverege developers",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -11,8 +11,7 @@
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/**/!(*e2e*).mjs'",
15
- "test-all": "mocha -t 60000 --exit 'test/**/**.mjs'",
14
+ "test": "mocha -t 60000 --exit 'test/**/*.mjs'",
16
15
  "prepack": "npm exec prepack"
17
16
  },
18
17
  "bin": {
@@ -104,4 +103,4 @@
104
103
  "mocha": "^11.7.5",
105
104
  "npm": "^11.6.2"
106
105
  }
107
- }
106
+ }
@@ -1,14 +1,12 @@
1
1
  /* eslint-disable security/detect-non-literal-fs-filename */
2
2
  import fs from 'node:fs'
3
- import path from 'node:path'
4
3
 
5
4
  import { shellCmd, log, debug, err } from '../Utils.mjs'
6
5
 
7
- import { parseGenericArtifactSchemaToRemoteRelativePath, parseGenericArtifactSchemaToLocalRelativePath, listGenericArtifactFiles, parseGenericArtifactFileToRemoteRelativePath, parseGenericArtifactSchemaToArtifactName } from './Utils.mjs'
6
+ import { toExplicitPath, deduplicatePath, convertGenericArtifactNameToPath } from './Utils.mjs'
8
7
 
9
8
  class ArtifactPuller {
10
- constructor( path, artifact ) {
11
- this.path = path
9
+ constructor( artifact ) {
12
10
  this.artifact = artifact
13
11
  }
14
12
 
@@ -19,101 +17,142 @@ class ArtifactPuller {
19
17
 
20
18
  class GenericArtifactPuller extends ArtifactPuller {
21
19
 
22
- async downloadFile( remoteRelativePath, localRelativePath ) {
23
- debug( { remoteRelativePath, localRelativePath }, '<==Download File' )
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 } )
24
24
 
25
- const isBasenameDifferent = path.basename( localRelativePath ) !== path.basename( remoteRelativePath )
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
+ }
26
33
 
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 } )
34
+ async cleanupFileDownload( tmpFilePath, finalFilePath ) {
35
+ // Delete the tmp file
36
+ if ( fs.existsSync( tmpFilePath ) ) {
37
+ fs.unlinkSync( tmpFilePath )
34
38
  }
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 )
35
53
 
36
54
  try {
37
55
  const command = `gcloud artifacts generic download \
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 )}`
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}`
45
63
 
46
64
  debug( { command }, '<==Download File Command' )
47
65
 
48
66
  await shellCmd( command )
49
67
 
50
- if ( isBasenameDifferent ) {
51
- await fs.promises.rename( tmpLocalRelativePath, localRelativePath )
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
52
82
  }
53
83
 
54
84
  return true
55
85
  } catch ( error ) {
56
- err( `Error downloading file: ${localRelativePath} ${error.message}` )
86
+ err( `Error downloading file: ${remoteFilePath} ${error.message}` )
57
87
  return false
88
+ } finally {
89
+ await this.cleanupFileDownload( tmpFilePath, localFilePath )
58
90
  }
59
91
  }
60
92
 
61
- async downloadDirectory( remoteRelativePath, localRelativePath ) {
62
-
63
- await fs.promises.mkdir( localRelativePath, { recursive : true } )
64
-
93
+ async downloadDirectory( localPath ) {
65
94
  const command = `gcloud artifacts generic download \
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}`
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}`
72
101
 
73
102
  debug( { command }, '<==Download Directory Command' )
74
103
 
75
104
  await shellCmd( command )
76
105
  }
77
106
 
78
- async downloadNestedDirectory( remoteRelativePath, localRelativePath ) {
79
- debug( { remoteRelativePath, localRelativePath }, '<==Download Nested Directory' )
107
+ async downloadNestedDirectory( remotePath, localPath ) {
108
+ debug( { remotePath, localPath }, '<==Download Nested Directory' )
80
109
 
81
- const files = await listGenericArtifactFiles( this.artifact )
110
+ const files = await this.listFiles( remotePath )
82
111
  await Promise.all(
83
112
  files
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
- }, [] )
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
+ } )
93
119
  )
94
120
  }
95
121
 
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
+
96
140
  async pull() {
97
141
  log( `Pulling artifact: ${this.artifact.name}` )
98
142
 
99
143
  debug( { artifact : this.artifact }, '<==Pulling artifact' )
100
144
 
101
145
  try {
102
- const remoteRelativePath = parseGenericArtifactSchemaToRemoteRelativePath( this.artifact )
103
- const localRelativePath = parseGenericArtifactSchemaToLocalRelativePath( this.artifact )
104
-
105
146
  if ( this.artifact.remote_file_path && this.artifact.local_file_path ) {
106
- await this.downloadFile( remoteRelativePath, path.join( this.path, localRelativePath ) )
147
+ await this.downloadFile( this.artifact.remote_file_path, toExplicitPath( this.artifact.local_file_path ) )
107
148
  } else if ( this.artifact.remote_file_path && this.artifact.local_path ) {
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 ) )
149
+ await this.downloadFile( this.artifact.remote_file_path, toExplicitPath( `${this.artifact.local_path}/${this.artifact.remote_file_path.split( '/' ).pop()}` ) )
111
150
  } else if ( this.artifact.remote_path && this.artifact.local_path ) {
112
- await this.downloadNestedDirectory( remoteRelativePath, path.join( this.path, localRelativePath ) )
151
+ await this.downloadNestedDirectory( this.artifact.remote_path, toExplicitPath( this.artifact.local_path ) )
113
152
  } else if ( this.artifact.remote_path && !this.artifact.local_path && !this.artifact.local_file_path ) {
114
- await this.downloadDirectory( remoteRelativePath, path.join( this.path, localRelativePath ) )
153
+ await this.downloadDirectory( this.artifact.remote_path, '.' )
115
154
  } else if ( !this.artifact.remote_path && !this.artifact.remote_file_path && this.artifact.local_path ) {
116
- await this.downloadDirectory( remoteRelativePath, path.join( this.path, localRelativePath ) )
155
+ await this.downloadDirectory( toExplicitPath( this.artifact.local_path ) )
117
156
  } else {
118
157
  throw new Error( 'No valid paths provided' )
119
158
  }
@@ -121,6 +160,13 @@ class GenericArtifactPuller extends ArtifactPuller {
121
160
  log( `Successfully pulled artifact: ${this.artifact.name}` )
122
161
 
123
162
  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
+ // }
124
170
  } catch ( error ) {
125
171
  err( `Error pulling artifact: ${this.artifact.name} ${error.message}` )
126
172
  }
@@ -1,20 +1,9 @@
1
- import path from 'node:path'
2
- import fs from 'node:fs'
3
-
4
1
  import { shellCmd, log, err, debug } from '../Utils.mjs'
5
2
 
6
- import {
7
- parseGenericArtifactFileToRemoteRelativePath,
8
- parseGenericArtifactSchemaToRemoteRelativePath,
9
- listGenericArtifactFiles,
10
- shouldUseIgnoreConfiguration,
11
- findIgnoreFilePath,
12
- copyFilesWithIgnoreConfiguration
13
- } from './Utils.mjs'
3
+ import { convertGenericArtifactNameToPath } from './Utils.mjs'
14
4
 
15
5
  class ArtifactPusher {
16
- constructor( path, artifact ) {
17
- this.path = path
6
+ constructor( artifact ) {
18
7
  this.artifact = artifact
19
8
  }
20
9
 
@@ -24,6 +13,25 @@ class ArtifactPusher {
24
13
  }
25
14
 
26
15
  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
+ }
27
35
 
28
36
  async push() {
29
37
  log( `Pushing artifact: ${this.artifact.name}` )
@@ -33,74 +41,41 @@ class GenericArtifactPusher extends ArtifactPusher {
33
41
  return false
34
42
  }
35
43
 
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 {
44
+ try {
45
+
42
46
  let command = ''
43
47
 
44
48
  command = `gcloud artifacts generic upload \
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
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}`
52
54
 
53
55
  if ( 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}`
56
+ command += ` --source-directory=${this.artifact.local_path}`
71
57
  command += ' --skip-existing'
72
58
  } else if ( this.artifact.local_file_path ) {
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
- }
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
84
63
  }
85
- const localPath = path.join( this.path, this.artifact.local_file_path )
86
- command += ` --source=${localPath}`
64
+ command += ` --source=${this.artifact.local_file_path}`
87
65
  }
88
66
 
89
- command += ` --destination-path=${this.artifact.remote_path || '""'}`
67
+ if ( this.artifact.remote_path ) {
68
+ command += ` --destination-path=${this.artifact.remote_path}`
69
+ }
90
70
 
91
71
  debug( { command }, '<==Upload Command' )
92
72
 
93
73
  await shellCmd( command )
94
-
95
- if ( pathToDelete ) {
96
- await fs.promises.rm( pathToDelete, { recursive : true } )
97
- }
98
-
74
+
99
75
  log( `Successfully pushed artifact: ${this.artifact.name}` )
100
76
 
101
77
  return true
102
78
  } catch ( error ) {
103
- debug( { error }, '<==Error Pushing Artifact' )
104
79
  err( `Error pushing artifact: ${this.artifact.name} ${error.message}` )
105
80
  return false
106
81
  }
@@ -1,42 +1,37 @@
1
- import path from 'node:path'
2
-
3
- import { assign, coerce, object, string, array, optional, enums, union, refine } from 'superstruct'
1
+ import { assign, object, string, array, optional, enums, union, refine } from 'superstruct'
4
2
 
5
3
  const ArtifactTypeEnum = {
6
4
  GENERIC : 'generic',
7
5
  }
8
6
 
9
- // Structs with the Partial prefix come from the need to validate the schema with defaults and without defaults merged together.
10
-
7
+ // Superstruct schema for individual artifact configuration
11
8
  const PartialArtifactSchema = object( {
12
9
  name : string(),
13
10
  type : optional( enums( Object.values( ArtifactTypeEnum ) ) ),
14
- project : optional( string() ),
15
- location : optional( string() ),
16
- repository : optional( string() ),
17
- package : optional( string() ),
18
- version : optional( string() )
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() )
19
16
  } )
20
17
 
21
18
  const ArtifactSchema = assign( PartialArtifactSchema, object( {
22
19
  name : string(),
23
20
  type : enums( Object.values( ArtifactTypeEnum ) ),
24
- project : string(),
25
- location : string(),
26
- repository : string(),
27
- package : string(),
28
- version : string()
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()
29
26
  } ) )
30
27
 
31
- const normalizedPath = () => coerce( string(), string(), value => path.normalize( value ) )
32
-
33
28
  const PartialGenericArtifactSchema = refine(
34
29
  assign( PartialArtifactSchema, object( {
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() )
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() )
40
35
  } ) ),
41
36
  'ConflictingPaths',
42
37
  ( value ) => {
@@ -52,11 +47,10 @@ const PartialGenericArtifactSchema = refine(
52
47
 
53
48
  const GenericArtifactSchema = refine(
54
49
  assign( ArtifactSchema, object( {
55
- type : enums( [ ArtifactTypeEnum.GENERIC ] ),
56
- local_file_path : optional( normalizedPath() ),
57
- local_path : optional( normalizedPath() ),
50
+ local_file_path : optional( string() ),
51
+ local_path : optional( string() ),
58
52
  remote_file_path : optional( string() ),
59
- remote_path : optional( normalizedPath() )
53
+ remote_path : optional( string() )
60
54
  } ) ),
61
55
  'ConflictingPaths',
62
56
  ( value ) => {
@@ -72,12 +66,11 @@ const GenericArtifactSchema = refine(
72
66
 
73
67
  // Superstruct schema for defaults configuration
74
68
  const DefaultsSchema = object( {
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() )
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() )
81
74
  } )
82
75
 
83
76
  // Superstruct schema for the complete artifacts configuration with defaults
@@ -1,19 +1,16 @@
1
1
  import fs from 'node:fs'
2
- import path from 'node:path'
3
2
  import crypto from 'crypto'
4
3
 
5
4
  import { validate } from 'superstruct'
6
5
 
7
- import { parseYamlFile, err, warning, debug, shellCmd } from '../Utils.mjs'
6
+ import { parseYamlFile, warning } from '../Utils.mjs'
8
7
 
9
8
  import { ArtifactTypeEnum, ArtifactsSchema, GenericArtifactSchema } from './Structs.mjs'
10
9
 
11
- const getArtifactsSchema = async ( artifactsFilePath = './artifacts.yaml' ) => {
10
+ const getArtifactsSchema = async ( artifactsFilePath = './artifacts.yml' ) => {
12
11
  const artifactsConfig = await parseYamlFile( artifactsFilePath )
13
12
  const [ error, result ] = validate( artifactsConfig, ArtifactsSchema )
14
13
  if ( error ) {
15
- err( `Invalid artifacts.yml file: ${error.message}` )
16
- debug( { err : error }, '<==Invalid artifacts.yml file' )
17
14
  throw new Error( `Invalid artifacts.yml file: ${error.message}` )
18
15
  }
19
16
  return result
@@ -25,7 +22,7 @@ const flattenArtifactsSchema = ( artifactsSchema ) => {
25
22
  ...artifactsSchema.defaults,
26
23
  ...artifact
27
24
  }
28
-
25
+
29
26
  let schema
30
27
  switch ( mergedArtifact.type ) {
31
28
  case ArtifactTypeEnum.GENERIC:
@@ -52,230 +49,44 @@ const flattenArtifactsSchema = ( artifactsSchema ) => {
52
49
  }, [] )
53
50
  }
54
51
 
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 )
52
+ const toImplicitPath = ( path ) => {
53
+ let implicitPath = path
54
+ // Remove leading './' if present
55
+ if ( implicitPath.startsWith( './' ) ) {
56
+ implicitPath = implicitPath.slice( 2 )
95
57
  }
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( '.' )
58
+ // Remove trailing slash
59
+ implicitPath = implicitPath.replace( /\/+$/, '' )
60
+ return implicitPath
103
61
  }
104
62
 
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 ) )
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}`
111
69
  }
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( '.' )
70
+ return explicitPath
119
71
  }
120
72
 
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( '/' )
73
+ const deduplicatePath = ( path ) => {
74
+ return path.replaceAll( '//', '/' )
132
75
  }
133
76
 
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( ':' )
77
+ const convertGenericArtifactNameToPath = ( genericArtifactName ) => {
78
+ return genericArtifactName.split( ':' ).pop().replaceAll( '%2F', '/' ).replaceAll( '%3A', ':' )
158
79
  }
159
80
 
160
- const calculateHash = async ( path ) => {
81
+ const calculateHash = ( path ) => {
161
82
  try {
162
83
  const hash = crypto.createHash( 'sha256' )
163
- hash.update( await fs.promises.readFile( path ) )
164
- return `${hash.digest( 'base64url' )}=`
84
+ hash.update( fs.readFileSync( path ) )
85
+ return hash.digest( 'base64url' )
165
86
  } catch ( error ) {
87
+ console.error( error )
166
88
  throw new Error( `Failed to calculate hash for artifact ${path}: ${error.message}` )
167
89
  }
168
90
  }
169
91
 
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
- }
92
+ export { calculateHash, getArtifactsSchema, flattenArtifactsSchema, toImplicitPath, toExplicitPath, deduplicatePath, convertGenericArtifactNameToPath }
@@ -1,7 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import path from 'node:path'
4
-
5
3
  import { program } from 'commander'
6
4
 
7
5
  import { warning, log } from '../Utils.mjs'
@@ -13,36 +11,33 @@ import { GenericArtifactPuller } from './ArtifactPuller.mjs'
13
11
  program
14
12
  .name( 'pull-artifact' )
15
13
  .description( 'Pull artifacts from GCP Artifact Registry' )
16
- .option( '-f, --file-path <file-path>', 'Path to artifacts.yaml file', './artifacts.yaml' )
17
- .option( '-a, --artifact-path <artifact-path>', 'GCP artifact path' )
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' )
18
17
  .parse()
19
18
 
20
19
  const options = program.opts()
21
20
 
22
21
  async function main() {
23
- const artifactsSchema = await getArtifactsSchema( options.filePath )
22
+ const artifactsYmlPath = options.filePath
23
+ const artifactPackageName = options.packageName || null
24
+ const artifactPackageVersion = options.packageVersion || null
25
+
26
+ const artifactsSchema = await getArtifactsSchema( artifactsYmlPath )
24
27
  const flattenedArtifactsSchema = flattenArtifactsSchema( artifactsSchema )
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
-
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 ) {
38
35
  filteredArtifactsSchema = flattenedArtifactsSchema.filter(
39
- artifact => artifact.project === project &&
40
- artifact.location === location &&
41
- artifact.repository === repository &&
42
- artifact.package === packageName &&
43
- artifact.version === version )
36
+ artifact => artifact.gcp_artifact_registry_package_name === artifactPackageName )
37
+ } else {
38
+ filteredArtifactsSchema = flattenedArtifactsSchema
44
39
  }
45
-
40
+
46
41
  let counter = 0
47
42
 
48
43
  const pullResults = await Promise.all(
@@ -50,7 +45,7 @@ async function main() {
50
45
  let puller
51
46
  switch ( artifactSchema.type ) {
52
47
  case ArtifactTypeEnum.GENERIC:
53
- puller = new GenericArtifactPuller( path.dirname( options.filePath ), artifactSchema )
48
+ puller = new GenericArtifactPuller( artifactSchema )
54
49
  break
55
50
  default:
56
51
  warning( `Unsupported artifact type "${artifactSchema.type}"` )
@@ -74,8 +69,6 @@ async function main() {
74
69
  counter = pullResults.filter( Boolean ).length
75
70
 
76
71
  log( `Successfully pulled ${counter}/${filteredArtifactsSchema.length} artifacts` )
77
-
78
- return counter === filteredArtifactsSchema.length
79
72
  }
80
73
 
81
74
  main()
@@ -1,7 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import path from 'node:path'
4
-
5
3
  import { program } from 'commander'
6
4
 
7
5
  import { warning, log } from '../Utils.mjs'
@@ -13,33 +11,31 @@ import { GenericArtifactPusher } from './ArtifactPusher.mjs'
13
11
  program
14
12
  .name( 'push-artifact' )
15
13
  .description( 'Push artifacts to GCP Artifact Registry' )
16
- .option( '-f, --file-path <file-path>', 'Path to artifacts.yaml file', './artifacts.yaml' )
17
- .option( '-a, --artifact-path <artifact-path>', 'GCP artifact path' )
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' )
18
17
  .parse()
19
18
 
20
19
  const options = program.opts()
21
20
 
22
21
  async function main() {
23
- const artifactsSchema = await getArtifactsSchema( options.filePath )
22
+ const artifactsYmlPath = options.filePath
23
+ const artifactPackageName = options.packageName || null
24
+ const artifactPackageVersion = options.packageVersion || null
25
+
26
+ const artifactsSchema = await getArtifactsSchema( artifactsYmlPath )
24
27
  const flattenedArtifactsSchema = flattenArtifactsSchema( artifactsSchema )
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
-
28
+
29
+ let filteredArtifactsSchema
30
+ if ( artifactPackageName && artifactPackageVersion ) {
37
31
  filteredArtifactsSchema = flattenedArtifactsSchema.filter(
38
- artifact => artifact.project === project &&
39
- artifact.location === location &&
40
- artifact.repository === repository &&
41
- artifact.package === packageName &&
42
- artifact.version === version )
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
43
39
  }
44
40
 
45
41
  let counter = 0
@@ -49,7 +45,7 @@ async function main() {
49
45
  let pusher
50
46
  switch ( artifactSchema.type ) {
51
47
  case ArtifactTypeEnum.GENERIC:
52
- pusher = new GenericArtifactPusher( path.dirname( options.filePath ), artifactSchema )
48
+ pusher = new GenericArtifactPusher( artifactSchema )
53
49
  break
54
50
  default:
55
51
  warning( `Unsupported artifact type "${artifactSchema.type}"` )
@@ -73,8 +69,6 @@ async function main() {
73
69
  counter = pushResults.filter( Boolean ).length
74
70
 
75
71
  log( `Successfully pushed ${counter}/${filteredArtifactsSchema.length} artifacts` )
76
-
77
- return counter === filteredArtifactsSchema.length
78
72
  }
79
73
 
80
74
  main()
package/src/bash-funcs CHANGED
@@ -305,6 +305,11 @@ PLUGIN_NOEXEC
305
305
  return 0
306
306
  }
307
307
 
308
+ # simple check to allow us to avoid double settings the k8s context
309
+ function ifNotFromHelmup() {
310
+ [ "$_FROM_HELMUP_" != "true" ]
311
+ }
312
+
308
313
  # simple enforcement of necessary tools that need to be installed
309
314
  function commandChecker() {
310
315
  for cmd in "$@"; do
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,7 +29,8 @@ K8SCALE_HELP
29
29
  exit 1
30
30
  esac
31
31
 
32
- overwhelm
32
+ [ ifNotFromHelmup ] && overwhelm
33
+
33
34
  [ $? -ne 0 ] && printf "\n\n***Aborting helmup...\n\n" && exit 1
34
35
 
35
36
  printf " scaled => `color g $DIRECTION`\n\n"
@@ -1,3 +0,0 @@
1
- {
2
- "editor.formatOnSave": true
3
- }