@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.
- package/.vscode/settings.json +3 -0
- package/README.md +18 -1
- package/package.json +8 -3
- package/src/Docker.mjs +16 -24
- package/src/Utils.mjs +11 -6
- package/src/artifacts/ArtifactPuller.mjs +177 -0
- package/src/artifacts/ArtifactPusher.mjs +86 -0
- package/src/artifacts/Structs.mjs +91 -0
- package/src/artifacts/Utils.mjs +92 -0
- package/src/artifacts/pull-artifact.mjs +74 -0
- package/src/artifacts/push-artifact.mjs +74 -0
- package/src/chart-to-registry.mjs +3 -3
- package/src/docker-to-registry.mjs +2 -9
- package/src/helm-audit.mjs +10 -2
- package/src/helm-charts/elasticsearch8/{elasticsearch-local-dev.yaml → elasticsearch-local.yaml} +9 -5
- package/src/helm-charts/elasticsearch8/helmup.plugin +2 -3
- package/src/templates/dockerfileTemplateNodeJs.hbs +1 -46
- package/leverege-build-tools-2.21.13.tgz +0 -0
- package/lib/server/build-tools.js +0 -138
- package/lib/server/firebaseDeploy.js +0 -195
- package/lib/server/firebaseServe.js +0 -116
- package/lib/server/getfbcfg.js +0 -25
- package/lib/server/getjson.js +0 -70
- package/lib/server/overwhelm.js +0 -447
- package/lib/server/push-my-chart.js +0 -18
- package/lib/server/refresh-npm-token.js +0 -135
- package/lib/server/tag-release.js +0 -681
- package/lib/server/unleash.js +0 -50
- package/lib/web/build-tools.js +0 -138
- package/lib/web/firebaseDeploy.js +0 -195
- package/lib/web/firebaseServe.js +0 -116
- package/lib/web/getfbcfg.js +0 -25
- package/lib/web/getjson.js +0 -70
- package/lib/web/overwhelm.js +0 -447
- package/lib/web/push-my-chart.js +0 -18
- package/lib/web/refresh-npm-token.js +0 -135
- package/lib/web/tag-release.js +0 -681
- package/lib/web/unleash.js +0 -50
- package/src/helm-charts/elasticsearch8/elasticsearch-local-prd.yaml +0 -124
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
|
|
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
|
|
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": "
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
210
|
-
fs.mkdirSync(
|
|
211
|
-
fs.renameSync(
|
|
212
|
-
fs.cpSync( './docker/Dockerfile',
|
|
213
|
-
fs.cpSync(
|
|
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',
|
|
214
|
+
fs.renameSync( './docker/bashrc', './build/bashrc' )
|
|
218
215
|
}
|
|
219
216
|
|
|
220
217
|
if ( isNodeJsProject ) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
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 =
|
|
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}
|
|
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}
|
|
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}
|
|
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 }
|