@leverege/build-tools 2.93.1 → 2.93.3-beta.1
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 +3 -2
- package/src/artifacts/ArtifactPuller.mjs +56 -102
- package/src/artifacts/ArtifactPusher.mjs +31 -35
- package/src/artifacts/Structs.mjs +32 -25
- package/src/artifacts/Utils.mjs +171 -25
- package/src/artifacts/pull-artifact.mjs +13 -19
- package/src/artifacts/push-artifact.mjs +12 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leverege/build-tools",
|
|
3
|
-
"version": "2.93.1",
|
|
3
|
+
"version": "2.93.3-beta.1",
|
|
4
4
|
"description": "A collection of build / support tools for Leverege developers",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -11,7 +11,8 @@
|
|
|
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
|
|
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": {
|
|
@@ -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 {
|
|
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
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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.
|
|
57
|
-
--location=${this.artifact.
|
|
58
|
-
--repository=${this.artifact.
|
|
59
|
-
--package=${this.artifact.
|
|
60
|
-
--version=${this.artifact.
|
|
61
|
-
--name="${
|
|
62
|
-
--destination=${
|
|
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
|
-
|
|
69
|
-
|
|
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: ${
|
|
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(
|
|
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.
|
|
96
|
-
--location=${this.artifact.
|
|
97
|
-
--repository=${this.artifact.
|
|
98
|
-
--package=${this.artifact.
|
|
99
|
-
--version=${this.artifact.
|
|
100
|
-
--destination=${
|
|
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(
|
|
108
|
-
debug( {
|
|
78
|
+
async downloadNestedDirectory( remoteRelativePath, localRelativePath ) {
|
|
79
|
+
debug( { remoteRelativePath, localRelativePath }, '<==Download Nested Directory' )
|
|
109
80
|
|
|
110
|
-
const files = await this.
|
|
81
|
+
const files = await listGenericArtifactFiles( this.artifact )
|
|
111
82
|
await Promise.all(
|
|
112
83
|
files
|
|
113
|
-
.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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(
|
|
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 {
|
|
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.
|
|
50
|
-
--location=${this.artifact.
|
|
51
|
-
--repository=${this.artifact.
|
|
52
|
-
--package=${this.artifact.
|
|
53
|
-
--version=${this.artifact.
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
60
|
+
const localPath = path.join( this.path, this.artifact.local_file_path )
|
|
61
|
+
command += ` --source=${localPath}`
|
|
65
62
|
}
|
|
66
63
|
|
|
67
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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(
|
|
32
|
-
local_path : optional(
|
|
33
|
-
remote_file_path : optional(
|
|
34
|
-
remote_path : optional(
|
|
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
|
-
|
|
51
|
-
|
|
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(
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
package/src/artifacts/Utils.mjs
CHANGED
|
@@ -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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
implicitPath = implicitPath.replace( /\/+$/, '' )
|
|
60
|
-
return implicitPath
|
|
102
|
+
return path.normalize( '.' )
|
|
61
103
|
}
|
|
62
104
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
|
74
|
-
|
|
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
|
|
78
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -12,32 +12,26 @@ program
|
|
|
12
12
|
.name( 'pull-artifact' )
|
|
13
13
|
.description( 'Pull artifacts from GCP Artifact Registry' )
|
|
14
14
|
.requiredOption( '-f, --file-path <path>', 'Path to artifacts.yaml file' )
|
|
15
|
-
.
|
|
16
|
-
.
|
|
15
|
+
.requiredOption( '-p, --project <project>', 'GCP project id' )
|
|
16
|
+
.requiredOption( '-l, --location <location>', 'GCP location' )
|
|
17
|
+
.requiredOption( '-r, --repository <repository>', 'GCP artifact registry repository' )
|
|
18
|
+
.requiredOption( '-n --package <name>', 'GCP artifact registry package name' )
|
|
19
|
+
.requiredOption( '-v, --version <version>', 'GCP artifact registry package version' )
|
|
17
20
|
.parse()
|
|
18
21
|
|
|
19
22
|
const options = program.opts()
|
|
20
23
|
|
|
21
24
|
async function main() {
|
|
22
|
-
const
|
|
23
|
-
const artifactPackageName = options.packageName || null
|
|
24
|
-
const artifactPackageVersion = options.packageVersion || null
|
|
25
|
-
|
|
26
|
-
const artifactsSchema = await getArtifactsSchema( artifactsYmlPath )
|
|
25
|
+
const artifactsSchema = await getArtifactsSchema( options.filePath )
|
|
27
26
|
const flattenedArtifactsSchema = flattenArtifactsSchema( artifactsSchema )
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
artifact => artifact.gcp_artifact_registry_package_name === artifactPackageName )
|
|
37
|
-
} else {
|
|
38
|
-
filteredArtifactsSchema = flattenedArtifactsSchema
|
|
39
|
-
}
|
|
40
|
-
|
|
28
|
+
const filteredArtifactsSchema = flattenedArtifactsSchema.filter(
|
|
29
|
+
artifact => artifact.project === options.project &&
|
|
30
|
+
artifact.location === options.location &&
|
|
31
|
+
artifact.repository === options.repository &&
|
|
32
|
+
artifact.package === options.package &&
|
|
33
|
+
artifact.version === options.version )
|
|
34
|
+
|
|
41
35
|
let counter = 0
|
|
42
36
|
|
|
43
37
|
const pullResults = await Promise.all(
|
|
@@ -12,31 +12,25 @@ program
|
|
|
12
12
|
.name( 'push-artifact' )
|
|
13
13
|
.description( 'Push artifacts to GCP Artifact Registry' )
|
|
14
14
|
.requiredOption( '-f, --file-path <path>', 'Path to artifacts.yaml file' )
|
|
15
|
-
.
|
|
16
|
-
.
|
|
15
|
+
.requiredOption( '-p, --project <project>', 'GCP project id' )
|
|
16
|
+
.requiredOption( '-l, --location <location>', 'GCP location' )
|
|
17
|
+
.requiredOption( '-r, --repository <repository>', 'GCP artifact registry repository' )
|
|
18
|
+
.requiredOption( '-n --package <name>', 'GCP artifact registry package name' )
|
|
19
|
+
.requiredOption( '-v, --version <version>', 'GCP artifact registry package version' )
|
|
17
20
|
.parse()
|
|
18
21
|
|
|
19
22
|
const options = program.opts()
|
|
20
23
|
|
|
21
24
|
async function main() {
|
|
22
|
-
const
|
|
23
|
-
const artifactPackageName = options.packageName || null
|
|
24
|
-
const artifactPackageVersion = options.packageVersion || null
|
|
25
|
-
|
|
26
|
-
const artifactsSchema = await getArtifactsSchema( artifactsYmlPath )
|
|
25
|
+
const artifactsSchema = await getArtifactsSchema( options.filePath )
|
|
27
26
|
const flattenedArtifactsSchema = flattenArtifactsSchema( artifactsSchema )
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
filteredArtifactsSchema = flattenedArtifactsSchema.filter(
|
|
36
|
-
artifact => artifact.gcp_artifact_registry_package_name === artifactPackageName )
|
|
37
|
-
} else {
|
|
38
|
-
filteredArtifactsSchema = flattenedArtifactsSchema
|
|
39
|
-
}
|
|
28
|
+
const filteredArtifactsSchema = flattenedArtifactsSchema.filter(
|
|
29
|
+
artifact => artifact.project === options.project &&
|
|
30
|
+
artifact.location === options.location &&
|
|
31
|
+
artifact.repository === options.repository &&
|
|
32
|
+
artifact.package === options.package &&
|
|
33
|
+
artifact.version === options.version )
|
|
40
34
|
|
|
41
35
|
let counter = 0
|
|
42
36
|
|