@leverege/build-tools 2.93.0-quinn.8 → 2.93.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.vscode/settings.json +3 -0
  2. package/README.md +18 -1
  3. package/package.json +8 -3
  4. package/src/Docker.mjs +16 -24
  5. package/src/Utils.mjs +11 -6
  6. package/src/artifacts/ArtifactPuller.mjs +177 -0
  7. package/src/artifacts/ArtifactPusher.mjs +86 -0
  8. package/src/artifacts/Structs.mjs +91 -0
  9. package/src/artifacts/Utils.mjs +92 -0
  10. package/src/artifacts/pull-artifact.mjs +74 -0
  11. package/src/artifacts/push-artifact.mjs +74 -0
  12. package/src/chart-to-registry.mjs +3 -3
  13. package/src/docker-to-registry.mjs +2 -9
  14. package/src/helm-audit.mjs +10 -2
  15. package/src/helm-charts/elasticsearch8/{elasticsearch-local-dev.yaml → elasticsearch-local.yaml} +9 -5
  16. package/src/helm-charts/elasticsearch8/helmup.plugin +2 -3
  17. package/src/templates/dockerfileTemplateNodeJs.hbs +1 -46
  18. package/leverege-build-tools-2.21.13.tgz +0 -0
  19. package/lib/server/build-tools.js +0 -138
  20. package/lib/server/firebaseDeploy.js +0 -195
  21. package/lib/server/firebaseServe.js +0 -116
  22. package/lib/server/getfbcfg.js +0 -25
  23. package/lib/server/getjson.js +0 -70
  24. package/lib/server/overwhelm.js +0 -447
  25. package/lib/server/push-my-chart.js +0 -18
  26. package/lib/server/refresh-npm-token.js +0 -135
  27. package/lib/server/tag-release.js +0 -681
  28. package/lib/server/unleash.js +0 -50
  29. package/lib/web/build-tools.js +0 -138
  30. package/lib/web/firebaseDeploy.js +0 -195
  31. package/lib/web/firebaseServe.js +0 -116
  32. package/lib/web/getfbcfg.js +0 -25
  33. package/lib/web/getjson.js +0 -70
  34. package/lib/web/overwhelm.js +0 -447
  35. package/lib/web/push-my-chart.js +0 -18
  36. package/lib/web/refresh-npm-token.js +0 -135
  37. package/lib/web/tag-release.js +0 -681
  38. package/lib/web/unleash.js +0 -50
  39. package/src/helm-charts/elasticsearch8/elasticsearch-local-prd.yaml +0 -124
@@ -0,0 +1,3 @@
1
+ {
2
+ "editor.formatOnSave": true
3
+ }
package/README.md CHANGED
@@ -18,7 +18,15 @@ npm install -g @leverege/build-tools
18
18
  ```bash
19
19
  . `build-tools --bashfun`
20
20
  ```
21
- * **docker-to-registry**: used to build and deploy docker images to the GCP image area
21
+ * **docker-to-registry**: used to build and deploy application docker images to the GCP artifact registry
22
+ * **pull-artifact**: used to pull generic artifacts from the GCP artifact registry
23
+ ```bash
24
+ Usage: pull-artifact -f <artifact schema file path> -n <package name> -v <package version>
25
+ ```
26
+ * **push-artifact**: used to push generic artifacts to the GCP artifact registry
27
+ ```bash
28
+ Usage: push-artifact -f <artifact schema file path> -n <package name> -v <package version>
29
+ ```
22
30
 
23
31
  ### Helm Helpers:
24
32
  * **overwhelm**: a k8s context helper and yaml replacer *${REPLACE_ME}*
@@ -57,6 +65,15 @@ Usage: k8scale <up|dn|down|roll> [services]
57
65
  * **prune-git**: performs maintenance on a list of git repositories.
58
66
  * **pull-git**: walks a list of git repos, pulling each if they are clean.
59
67
 
68
+ ### Troubleshooting:
69
+
70
+ For additional debug level logging, set the environment variable `BUILD_TOOLS_DEBUG=1` before running the tool.
71
+
72
+ Example:
73
+ ```bash
74
+ BUILD_TOOLS_DEBUG=1 docker-to-registry
75
+ ```
76
+
60
77
  ## Authors
61
78
 
62
79
  * **DevOps and Friends**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leverege/build-tools",
3
- "version": "2.93.0-quinn.8",
3
+ "version": "2.93.0",
4
4
  "description": "A collection of build / support tools for Leverege developers",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -11,7 +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": "echo \"no tests specified\" && exit 0",
14
+ "test": "mocha -t 60000 --exit 'test/**/*.mjs'",
15
15
  "prepack": "npm exec prepack"
16
16
  },
17
17
  "bin": {
@@ -54,7 +54,9 @@
54
54
  "pkglint": "src/pkglint.sh",
55
55
  "prepack": "src/prepack.mjs",
56
56
  "prune-git": "src/prune-git.sh",
57
+ "pull-artifact": "src/artifacts/pull-artifact.mjs",
57
58
  "pull-git": "src/pull-git.sh",
59
+ "push-artifact": "src/artifacts/push-artifact.mjs",
58
60
  "push-my-chart": "src/push-my-chart.mjs",
59
61
  "refresh-npm-token": "src/refresh-npm-token.mjs",
60
62
  "refresh-py-idx": "src/refresh-py-idx.sh",
@@ -91,11 +93,14 @@
91
93
  "shell-quote": "^1.8.3",
92
94
  "simple-git": "^3.30.0",
93
95
  "sloc": "^0.3.2",
96
+ "superstruct": "^2.0.2",
94
97
  "toml": "^3.0.0",
95
98
  "zx": "^8.8.5"
96
99
  },
97
100
  "devDependencies": {
98
101
  "@leverege/eslint-config-leverege": "^5.1.1",
102
+ "chai": "^6.2.1",
103
+ "mocha": "^11.7.5",
99
104
  "npm": "^11.6.2"
100
105
  }
101
- }
106
+ }
package/src/Docker.mjs CHANGED
@@ -121,14 +121,13 @@ const generateDockerfile = async ( options ) => {
121
121
  fs.writeFileSync( dockerfilePlugin, dockerfilePluginTemplate )
122
122
  }
123
123
 
124
- const buildDir = options.isYarnProject ? `/tmp/${options.containerName}/build` : './build'
125
124
  try {
126
- const build = fs.readdirSync( buildDir )
125
+ const build = fs.readdirSync( './build' )
127
126
  if ( build.length === 0 ) {
128
127
  errorExit( `***ERROR: the build dir is empty - ${chalk.yellow( 'npm run build' )}` )
129
128
  }
130
129
  } catch ( err ) {
131
- errorExit( `***ERROR: expected the ${buildDir} directory to exist - ${chalk.yellow( 'npm run build' )}` )
130
+ errorExit( `***ERROR: expected the ./build directory to exist - ${chalk.yellow( 'npm run build' )}` )
132
131
  }
133
132
 
134
133
  // At this point the docker plugin file better exist.
@@ -183,10 +182,8 @@ const buildContainerImage = async ( repoDescr ) => {
183
182
  isNodeJsProject,
184
183
  isNpmWorkspace,
185
184
  isPythonProject,
186
- isYarnProject
187
185
  } = repoDescr
188
- const buildDir = isYarnProject ? `/tmp/${containerName}/build` : './build'
189
- const buildWorkspace = `${buildDir}/workspace` // passed to Cloud Build
186
+ const buildWorkspace = './build/workspace' // passed to Cloud Build
190
187
  const imageName = `${artifactRegistry}/images/${containerName}`
191
188
 
192
189
  if ( fs.existsSync( buildWorkspace ) ) {
@@ -206,28 +203,23 @@ const buildContainerImage = async ( repoDescr ) => {
206
203
  // 5. drop a copy of dev's npmrc to use for build process
207
204
  // 6. drop a copy of package.json and the lock into the image root
208
205
  //
209
- fs.renameSync( buildDir, path.join( path.dirname( buildDir ), 'workspace' ) )
210
- fs.mkdirSync( buildDir )
211
- fs.renameSync( path.join( path.dirname( buildDir ), 'workspace' ), buildWorkspace )
212
- fs.cpSync( './docker/Dockerfile', `${buildDir}/Dockerfile` )
213
- fs.cpSync( `${buildDir}/Dockerfile`, `${buildWorkspace}/Dockerfile` )
206
+ fs.renameSync( './build', './workspace' )
207
+ fs.mkdirSync( './build' )
208
+ fs.renameSync( './workspace', buildWorkspace )
209
+ fs.cpSync( './docker/Dockerfile', './build/Dockerfile' )
210
+ fs.cpSync( './build/Dockerfile', `${buildWorkspace}/Dockerfile` )
214
211
 
215
212
  // bashrc won't exist if we're opting out of dockerfile generation
216
213
  if ( fs.existsSync( './docker/bashrc' ) ) {
217
- fs.renameSync( './docker/bashrc', `${buildDir}/bashrc` )
214
+ fs.renameSync( './docker/bashrc', './build/bashrc' )
218
215
  }
219
216
 
220
217
  if ( isNodeJsProject ) {
221
-
222
- if ( isYarnProject ) {
223
- fs.cpSync( `${process.env.HOME}/.yarnrc.yml`, `${buildDir}/.yarnrc.yml` )
224
- } else {
225
- fs.cpSync( './package.json', `${buildWorkspace}/package.json` )
226
- fs.cpSync( `${process.env.HOME}/.npmrc`, `${buildDir}/.npmrc` )
227
- fs.cpSync( './package-lock.json', `${buildWorkspace}/package-lock.json` )
228
- }
218
+ fs.cpSync( `${process.env.HOME}/.npmrc`, './build/.npmrc' )
219
+ fs.cpSync( './package.json', `${buildWorkspace}/package.json` )
220
+ fs.cpSync( './package-lock.json', `${buildWorkspace}/package-lock.json` )
229
221
  }
230
- if ( isNpmWorkspace && !isYarnProject ) {
222
+ if ( isNpmWorkspace ) {
231
223
  fs.rm( './package-lock.json', ( err ) => {} ) // no leaf node locks in workspaces
232
224
  }
233
225
 
@@ -239,7 +231,7 @@ const buildContainerImage = async ( repoDescr ) => {
239
231
  const gcloudProjectNumber = await shellCmd( `gcloud projects describe ${artifactProject} --format="value(projectNumber)"` )
240
232
 
241
233
  // build the cloudbuild.yaml from the handlebars template
242
- const cloudBuildFile = `${buildDir}/cloudbuild.yaml`
234
+ const cloudBuildFile = './build/cloudbuild.yaml'
243
235
  let cloudBuildSteps
244
236
  let imagePlatformString
245
237
  if ( isPythonProject || ( imagePlatform.length === 1 && imagePlatform[0] === 'linux/arm64' ) || imagePlatform.length > 1 ) {
@@ -269,12 +261,12 @@ const buildContainerImage = async ( repoDescr ) => {
269
261
  ` ) )
270
262
 
271
263
  if ( process.env.DRY_RUN === '1' ) {
272
- log( `Would have run => ${cloudBuildCmd} ${cloudBuildLogs} ${cloudBuildCfg} ${buildDir}` )
264
+ log( `Would have run => ${cloudBuildCmd} ${cloudBuildLogs} ${cloudBuildCfg} ./build` )
273
265
  log( '\n***Exiting from DRY_RUN\n' )
274
266
  process.exit( 0 )
275
267
  }
276
268
 
277
- await shellCmd( `${cloudBuildCmd} ${cloudBuildLogs} ${cloudBuildCfg} ${buildDir}`, { stdio : 'inherit' } )
269
+ await shellCmd( `${cloudBuildCmd} ${cloudBuildLogs} ${cloudBuildCfg} ./build`, { stdio : 'inherit' } )
278
270
  }
279
271
 
280
272
  const repoCleanup = async () => {
package/src/Utils.mjs CHANGED
@@ -104,7 +104,7 @@ export const establishOauthAccess = async ( registryLocation = 'us-docker.pkg.de
104
104
  //
105
105
  // Get the current access token and use it to login to the registry with helm.
106
106
  try {
107
- const accessToken = await shellCmd( 'gcloud auth print-access-token' )
107
+ const accessToken = ( await shellCmd( 'gcloud auth print-access-token' ) ).trim()
108
108
  // -p is frowned upon but using a pipe | is problematic with shellCmd (execa)
109
109
  const registryLoginCommand = `helm registry login -u oauth2accesstoken -p ${accessToken}`
110
110
 
@@ -224,6 +224,15 @@ export const gitRepoIsDirty = async () => {
224
224
  return cleanliness?.length > 0
225
225
  }
226
226
 
227
+ export const parseYamlFile = async ( filePath ) => {
228
+ try {
229
+ const fileContent = await fs.promises.readFile( filePath, 'utf8' )
230
+ return YAML.load( fileContent )
231
+ } catch ( error ) {
232
+ throw new Error( `Failed to parse YAML file ${filePath}: ${error.message}` )
233
+ }
234
+ }
235
+
227
236
  export const parseJsonFile = async ( jsonFile ) => {
228
237
  // TODO: rework this file exists logic / throw
229
238
  if ( !fs.existsSync( jsonFile ) ) { return undefined } // eslint-disable-line security/detect-non-literal-fs-filename
@@ -525,11 +534,10 @@ export const getRepositoryDescr = async () => {
525
534
  const gitRoot = await getGitRootDirectory()
526
535
  const gitHooksPath = await getGitHooksPath()
527
536
  const { branch : gitBranch, upstream : gitUpstream } = await getGitBranchAndUpstream()
528
- const subPkgs = await glob( `${gitRoot}/**/**/package.json`, { ignore : '/**/node_modules/**' } )
537
+ const subPkgs = await glob( `${gitRoot}/packages/**/package.json`, { ignore : '/**/node_modules/**' } )
529
538
  const helmChart = await parseHelmChart()
530
539
  const rootPackageJson = await parseJsonFile( `${gitRoot}/package.json` )
531
540
  const rootPyProjectToml = await parsePyProjectToml( `${gitRoot}/pyproject.toml` )
532
- const isYarnProject = fs.existsSync( path.join( gitRoot, 'yarn.lock' ) ) || fs.existsSync( path.join( gitRoot, '.yarn' ) )
533
541
 
534
542
  return {
535
543
  currentDir : process.cwd(),
@@ -547,7 +555,6 @@ export const getRepositoryDescr = async () => {
547
555
  rootPackageJson,
548
556
  rootPyProjectToml,
549
557
  subPkgs,
550
- isYarnProject
551
558
  }
552
559
  }
553
560
 
@@ -562,7 +569,6 @@ export const analyzeRepository = async ( giveGuidance ) => {
562
569
  rootPackageJson,
563
570
  rootPyProjectToml,
564
571
  isNpmWorkspace,
565
- isYarnProject
566
572
  } = await getRepositoryDescr()
567
573
 
568
574
  if ( isMonoRepo && rootPackageJson.isInvalid ) {
@@ -671,6 +677,5 @@ export const analyzeRepository = async ( giveGuidance ) => {
671
677
  packageVersion,
672
678
  subPkgs,
673
679
  wsName,
674
- isYarnProject
675
680
  }
676
681
  }
@@ -0,0 +1,177 @@
1
+ /* eslint-disable security/detect-non-literal-fs-filename */
2
+ import fs from 'node:fs'
3
+
4
+ import { shellCmd, log, debug, err } from '../Utils.mjs'
5
+
6
+ import { toExplicitPath, deduplicatePath, convertGenericArtifactNameToPath } from './Utils.mjs'
7
+
8
+ class ArtifactPuller {
9
+ constructor( artifact ) {
10
+ this.artifact = artifact
11
+ }
12
+
13
+ async pull() {
14
+ throw new Error( 'Not implemented' )
15
+ }
16
+ }
17
+
18
+ class GenericArtifactPuller extends ArtifactPuller {
19
+
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
+
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
+ }
33
+
34
+ async cleanupFileDownload( tmpFilePath, finalFilePath ) {
35
+ // Delete the tmp file
36
+ if ( fs.existsSync( tmpFilePath ) ) {
37
+ fs.unlinkSync( tmpFilePath )
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 )
53
+
54
+ try {
55
+ 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}`
63
+
64
+ debug( { command }, '<==Download File Command' )
65
+
66
+ await shellCmd( command )
67
+
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
82
+ }
83
+
84
+ return true
85
+ } catch ( error ) {
86
+ err( `Error downloading file: ${remoteFilePath} ${error.message}` )
87
+ return false
88
+ } finally {
89
+ await this.cleanupFileDownload( tmpFilePath, localFilePath )
90
+ }
91
+ }
92
+
93
+ async downloadDirectory( localPath ) {
94
+ 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}`
101
+
102
+ debug( { command }, '<==Download Directory Command' )
103
+
104
+ await shellCmd( command )
105
+ }
106
+
107
+ async downloadNestedDirectory( remotePath, localPath ) {
108
+ debug( { remotePath, localPath }, '<==Download Nested Directory' )
109
+
110
+ const files = await this.listFiles( remotePath )
111
+ await Promise.all(
112
+ 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
+ } )
119
+ )
120
+ }
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
+
140
+ async pull() {
141
+ log( `Pulling artifact: ${this.artifact.name}` )
142
+
143
+ debug( { artifact : this.artifact }, '<==Pulling artifact' )
144
+
145
+ try {
146
+ 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 ) )
148
+ } 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()}` ) )
150
+ } else if ( this.artifact.remote_path && this.artifact.local_path ) {
151
+ await this.downloadNestedDirectory( this.artifact.remote_path, toExplicitPath( this.artifact.local_path ) )
152
+ } else if ( this.artifact.remote_path && !this.artifact.local_path && !this.artifact.local_file_path ) {
153
+ await this.downloadDirectory( this.artifact.remote_path, '.' )
154
+ } else if ( !this.artifact.remote_path && !this.artifact.remote_file_path && this.artifact.local_path ) {
155
+ await this.downloadDirectory( toExplicitPath( this.artifact.local_path ) )
156
+ } else {
157
+ throw new Error( 'No valid paths provided' )
158
+ }
159
+
160
+ log( `Successfully pulled artifact: ${this.artifact.name}` )
161
+
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
+ // }
170
+ } catch ( error ) {
171
+ err( `Error pulling artifact: ${this.artifact.name} ${error.message}` )
172
+ }
173
+ return false
174
+ }
175
+ }
176
+
177
+ export { ArtifactPuller, GenericArtifactPuller }
@@ -0,0 +1,86 @@
1
+ /* eslint-disable security/detect-non-literal-fs-filename */
2
+ import { shellCmd, log, err, debug } from '../Utils.mjs'
3
+
4
+ import { convertGenericArtifactNameToPath } from './Utils.mjs'
5
+
6
+ class ArtifactPusher {
7
+ constructor( artifact ) {
8
+ this.artifact = artifact
9
+ }
10
+
11
+ async push() {
12
+ throw new Error( 'Not implemented' )
13
+ }
14
+ }
15
+
16
+ class GenericArtifactPusher extends ArtifactPusher {
17
+
18
+ async listFiles( ) {
19
+ const command = `gcloud artifacts files list \
20
+ --project=${this.artifact.gcp_project_id} \
21
+ --location=${this.artifact.gcp_location} \
22
+ --repository=${this.artifact.gcp_artifact_registry_repository_name} \
23
+ --package=${this.artifact.gcp_artifact_registry_package_name} \
24
+ --version ${this.artifact.gcp_artifact_registry_package_version} \
25
+ --format="json"`
26
+
27
+ debug( { command }, '<==List Files Command' )
28
+
29
+ const result = await shellCmd( command )
30
+
31
+ // Parse the JSON response
32
+ const files = JSON.parse( result )
33
+
34
+ return files
35
+ }
36
+
37
+ async push() {
38
+ log( `Pushing artifact: ${this.artifact.name}` )
39
+
40
+ if ( this.artifact.remote_file_path ) {
41
+ err( 'Remote file path is not supported for pushing artifacts' )
42
+ return false
43
+ }
44
+
45
+ try {
46
+
47
+ let command = ''
48
+
49
+ command = `gcloud artifacts generic upload \
50
+ --project=${this.artifact.gcp_project_id} \
51
+ --location=${this.artifact.gcp_location} \
52
+ --repository=${this.artifact.gcp_artifact_registry_repository_name} \
53
+ --package=${this.artifact.gcp_artifact_registry_package_name} \
54
+ --version=${this.artifact.gcp_artifact_registry_package_version}`
55
+
56
+ if ( this.artifact.local_path ) {
57
+ command += ` --source-directory=${this.artifact.local_path}`
58
+ command += ' --skip-existing'
59
+ } else if ( this.artifact.local_file_path ) {
60
+ const files = await this.listFiles( this.artifact.remote_path )
61
+ if ( files.some( file => convertGenericArtifactNameToPath( file.name ) === `${this.artifact.remote_path}/${this.artifact.remote_file_path.split( '/' ).pop()}` ) ) {
62
+ err( `File already exists in the artifact registry: ${this.artifact.local_file_path}` )
63
+ return false
64
+ }
65
+ command += ` --source=${this.artifact.local_file_path}`
66
+ }
67
+
68
+ if ( this.artifact.remote_path ) {
69
+ command += ` --destination-path=${this.artifact.remote_path}`
70
+ }
71
+
72
+ debug( { command }, '<==Upload Command' )
73
+
74
+ await shellCmd( command )
75
+
76
+ log( `Successfully pushed artifact: ${this.artifact.name}` )
77
+
78
+ return true
79
+ } catch ( error ) {
80
+ err( `Error pushing artifact: ${this.artifact.name} ${error.message}` )
81
+ return false
82
+ }
83
+ }
84
+ }
85
+
86
+ export { ArtifactPusher, GenericArtifactPusher }
@@ -0,0 +1,91 @@
1
+ import { assign, object, string, array, optional, enums, union, refine } from 'superstruct'
2
+
3
+ const ArtifactTypeEnum = {
4
+ GENERIC : 'generic',
5
+ }
6
+
7
+ // Superstruct schema for individual artifact configuration
8
+ const PartialArtifactSchema = object( {
9
+ name : string(),
10
+ 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() )
16
+ } )
17
+
18
+ const ArtifactSchema = assign( PartialArtifactSchema, object( {
19
+ name : string(),
20
+ 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()
26
+ } ) )
27
+
28
+ const PartialGenericArtifactSchema = refine(
29
+ 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
+ } ) ),
36
+ 'ConflictingPaths',
37
+ ( value ) => {
38
+ if ( value.local_file_path && value.local_path ) {
39
+ return 'Only one of local_file_path or local_path must be provided'
40
+ }
41
+ if ( value.remote_file_path && value.remote_path ) {
42
+ return 'Only one of remote_file_path or remote_path must be provided'
43
+ }
44
+ return true
45
+ }
46
+ )
47
+
48
+ const GenericArtifactSchema = refine(
49
+ assign( ArtifactSchema, object( {
50
+ local_file_path : optional( string() ),
51
+ local_path : optional( string() ),
52
+ remote_file_path : optional( string() ),
53
+ remote_path : optional( string() )
54
+ } ) ),
55
+ 'ConflictingPaths',
56
+ ( value ) => {
57
+ if ( value.local_file_path && value.local_path ) {
58
+ return 'Only one of local_file_path or local_path must be provided'
59
+ }
60
+ if ( value.remote_file_path && value.remote_path ) {
61
+ return 'Only one of remote_file_path or remote_path must be provided'
62
+ }
63
+ return true
64
+ }
65
+ )
66
+
67
+ // Superstruct schema for defaults configuration
68
+ 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() )
74
+ } )
75
+
76
+ // Superstruct schema for the complete artifacts configuration with defaults
77
+ const ArtifactsSchema = object( {
78
+ defaults : optional( DefaultsSchema ),
79
+ artifacts : array( union( [ PartialArtifactSchema, PartialGenericArtifactSchema ] ) )
80
+ } )
81
+
82
+ const FlattenedArtifactsSchema = array( assign( union( [ ArtifactSchema, GenericArtifactSchema ] ) ) )
83
+
84
+ export {
85
+ ArtifactTypeEnum,
86
+ ArtifactsSchema,
87
+ FlattenedArtifactsSchema,
88
+ ArtifactSchema,
89
+ DefaultsSchema,
90
+ GenericArtifactSchema,
91
+ }
@@ -0,0 +1,92 @@
1
+ import fs from 'node:fs'
2
+ import crypto from 'crypto'
3
+
4
+ import { validate } from 'superstruct'
5
+
6
+ import { parseYamlFile, warning } from '../Utils.mjs'
7
+
8
+ import { ArtifactTypeEnum, ArtifactsSchema, GenericArtifactSchema } from './Structs.mjs'
9
+
10
+ const getArtifactsSchema = async ( artifactsFilePath = './artifacts.yml' ) => {
11
+ const artifactsConfig = await parseYamlFile( artifactsFilePath )
12
+ const [ error, result ] = validate( artifactsConfig, ArtifactsSchema )
13
+ if ( error ) {
14
+ throw new Error( `Invalid artifacts.yml file: ${error.message}` )
15
+ }
16
+ return result
17
+ }
18
+
19
+ const flattenArtifactsSchema = ( artifactsSchema ) => {
20
+ return artifactsSchema.artifacts.reduce( ( flattenedArtifacts, artifact ) => {
21
+ const mergedArtifact = {
22
+ ...artifactsSchema.defaults,
23
+ ...artifact
24
+ }
25
+
26
+ let schema
27
+ switch ( mergedArtifact.type ) {
28
+ case ArtifactTypeEnum.GENERIC:
29
+ schema = GenericArtifactSchema
30
+ break
31
+ default:
32
+ warning( `Unsupported artifact type "${mergedArtifact.type}"` )
33
+ break
34
+ }
35
+
36
+ if ( !schema ) {
37
+ warning( `Invalid artifact type "${mergedArtifact.type}"` )
38
+ return flattenedArtifacts
39
+ }
40
+
41
+ const [ error, validatedArtifact ] = validate( mergedArtifact, schema )
42
+
43
+ if ( error ) {
44
+ warning( `Invalid artifact "${mergedArtifact.name}": ${error.message}` )
45
+ return flattenedArtifacts
46
+ }
47
+
48
+ return [ ...flattenedArtifacts, validatedArtifact ]
49
+ }, [] )
50
+ }
51
+
52
+ const toImplicitPath = ( path ) => {
53
+ let implicitPath = path
54
+ // Remove leading './' if present
55
+ if ( implicitPath.startsWith( './' ) ) {
56
+ implicitPath = implicitPath.slice( 2 )
57
+ }
58
+ // Remove trailing slash
59
+ implicitPath = implicitPath.replace( /\/+$/, '' )
60
+ return implicitPath
61
+ }
62
+
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}`
69
+ }
70
+ return explicitPath
71
+ }
72
+
73
+ const deduplicatePath = ( path ) => {
74
+ return path.replaceAll( '//', '/' )
75
+ }
76
+
77
+ const convertGenericArtifactNameToPath = ( genericArtifactName ) => {
78
+ return genericArtifactName.split( ':' ).pop().replaceAll( '%2F', '/' ).replaceAll( '%3A', ':' )
79
+ }
80
+
81
+ const calculateHash = ( path ) => {
82
+ try {
83
+ const hash = crypto.createHash( 'sha256' )
84
+ hash.update( fs.readFileSync( path ) )
85
+ return hash.digest( 'base64url' )
86
+ } catch ( error ) {
87
+ console.error( error )
88
+ throw new Error( `Failed to calculate hash for artifact ${path}: ${error.message}` )
89
+ }
90
+ }
91
+
92
+ export { calculateHash, getArtifactsSchema, flattenArtifactsSchema, toImplicitPath, toExplicitPath, deduplicatePath, convertGenericArtifactNameToPath }