@leverege/build-tools 2.91.1 → 2.93.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ {
2
+ "editor.formatOnSave": true
3
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leverege/build-tools",
3
- "version": "2.91.1",
3
+ "version": "2.93.0-beta.10",
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/Utils.mjs CHANGED
@@ -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
@@ -0,0 +1,21 @@
1
+ import crypto from 'crypto'
2
+ import fs from 'fs'
3
+
4
+ class ArtifactPusher {
5
+ constructor( artifact ) {
6
+ this.artifact = artifact
7
+ }
8
+
9
+ async pull() {
10
+ throw new Error( 'Not implemented' )
11
+ }
12
+ }
13
+
14
+ class GenericArtifactPusher extends ArtifactPusher {
15
+
16
+ async pull() {
17
+ throw new Error( 'Not implemented' )
18
+ }
19
+ }
20
+
21
+ export { ArtifactPusher, GenericArtifactPusher }
@@ -0,0 +1,133 @@
1
+ import crypto from 'crypto'
2
+ import fs from 'fs'
3
+
4
+ class ArtifactPusher {
5
+ constructor( artifact ) {
6
+ this.artifact = artifact
7
+ }
8
+
9
+ calculateHash() {
10
+ try {
11
+ const hash = crypto.createHash( 'sha256' )
12
+ hash.update( fs.readFileSync( this.artifact.source_path ) )
13
+ return hash.digest( 'base64url' )
14
+ } catch ( error ) {
15
+ console.error( error )
16
+ throw new Error( `Failed to calculate hash for artifact ${this.artifact.name}: ${error.message}` )
17
+ }
18
+ }
19
+
20
+ async exists() {
21
+ throw new Error( 'Not implemented' )
22
+ }
23
+
24
+ async push() {
25
+ throw new Error( 'Not implemented' )
26
+ }
27
+ }
28
+
29
+ class GenericArtifactPusher extends ArtifactPusher {
30
+
31
+ async exists() {
32
+ const isDirectory = fs.statSync( this.artifact.source_path ).isDirectory()
33
+
34
+ // If source path is a directory, check whether the destination path alreadu exists in the registry
35
+ if ( isDirectory ) {
36
+ // For the time being, return false, and let gcloud skip existing files in upload-time.
37
+ // It's pretty slow, might be worth to break down the directory into individual artifacts
38
+ // and manage each one with its own pusher.
39
+ return false
40
+ }
41
+
42
+ // Check if artifact already exists in the registry
43
+ console.log( 'Checking if artifact already exists in the registry...' )
44
+
45
+ const artifactHash = this.calculateHash()
46
+
47
+ const command = `gcloud artifacts files list \
48
+ --project=${this.artifact.gcp_project_id} \
49
+ --location=${this.artifact.gcp_location} \
50
+ --repository=${this.artifact.gcp_artifact_registry_repository_name} \
51
+ --package=${this.artifact.gcp_artifact_registry_package_name} \
52
+ --version ${this.artifact.gcp_artifact_registry_package_version} \
53
+ --filter="name:${this.artifact.destination_path}" \
54
+ --format="json"`
55
+
56
+ console.log( 'Command:', command.replace( /\s+/g, ' ' ).trim() )
57
+
58
+ try {
59
+ const { execSync } = await import( 'child_process' )
60
+ const result = execSync( command, { encoding : 'utf8', stdio : 'pipe' } )
61
+
62
+ // Parse the JSON response
63
+ const files = JSON.parse( result )
64
+
65
+ // Check if any files were found and match our hash
66
+ if ( files && files.length > 0 ) {
67
+ for ( const file of files ) {
68
+ for ( const hash of file.hashes ) {
69
+ // Compare without padding since base64url may omit trailing '='
70
+ const normalizedOurHash = artifactHash.replace( /=+$/, '' )
71
+ const normalizedRegistryHash = hash.value.replace( /=+$/, '' )
72
+ if ( hash.type === 'SHA256' && normalizedRegistryHash === normalizedOurHash ) {
73
+ console.log( `Artifact already exists in the registry: ${this.artifact.gcp_artifact_registry_package_name}:${this.artifact.gcp_artifact_registry_package_version}` )
74
+ return true
75
+ }
76
+ }
77
+ }
78
+ }
79
+ console.log( 'Artifact does not exist in the registry' )
80
+ return false
81
+
82
+ } catch ( error ) {
83
+ console.error( 'Error checking artifact existence:', error.message )
84
+ throw error
85
+ }
86
+ }
87
+
88
+ async push() {
89
+ console.log( 'Publishing artifact to registry...' )
90
+
91
+ if ( await this.exists() ) {
92
+ console.log( 'Artifact already exists in the registry' )
93
+ return false
94
+ }
95
+
96
+ console.log( 'Uploading artifact to registry...' )
97
+
98
+ let command = ''
99
+
100
+ command = `gcloud artifacts generic upload \
101
+ --project=${this.artifact.gcp_project_id} \
102
+ --location=${this.artifact.gcp_location} \
103
+ --repository=${this.artifact.gcp_artifact_registry_repository_name} \
104
+ --package=${this.artifact.gcp_artifact_registry_package_name} \
105
+ --version=${this.artifact.gcp_artifact_registry_package_version}`
106
+
107
+ const isDirectory = fs.statSync( this.artifact.source_path ).isDirectory()
108
+ if ( isDirectory ) {
109
+ command += ` --source-directory=${this.artifact.source_path}`
110
+ command += ' --skip-existing'
111
+ } else {
112
+ command += ` --source=${this.artifact.source_path}`
113
+ }
114
+
115
+ if ( this.artifact.destination_path ) {
116
+ command += ` --destination-path=${this.artifact.destination_path}`
117
+ }
118
+
119
+ console.log( 'Command:', command.replace( /\s+/g, ' ' ).trim() )
120
+
121
+ try {
122
+ const { execSync } = await import( 'child_process' )
123
+ execSync( command, { encoding : 'utf8', stdio : 'pipe' } )
124
+ console.log( `Successfully published artifact: ${this.artifact.gcp_artifact_registry_package_name}:${this.artifact.gcp_artifact_registry_package_version}` )
125
+ return true
126
+ } catch ( error ) {
127
+ console.error( 'Error publishing artifact:', error.message )
128
+ return false
129
+ }
130
+ }
131
+ }
132
+
133
+ export { ArtifactPusher, GenericArtifactPusher }
@@ -0,0 +1,63 @@
1
+ import { assign, object, string, array, optional, enums, union } 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 = assign( PartialArtifactSchema, object( {
29
+ type : enums( [ ArtifactTypeEnum.GENERIC ] ),
30
+ source_path : string(),
31
+ destination_path : string()
32
+ } ) )
33
+
34
+ const GenericArtifactSchema = assign( ArtifactSchema, object( {
35
+ source_path : string(),
36
+ destination_path : optional( string() )
37
+ } ) )
38
+
39
+ // Superstruct schema for defaults configuration
40
+ const DefaultsSchema = object( {
41
+ gcp_project_id : optional( string() ),
42
+ gcp_location : optional( string() ),
43
+ gcp_artifact_registry_repository_name : optional( string() ),
44
+ gcp_artifact_registry_package_name : optional( string() ),
45
+ gcp_artifact_registry_package_version : optional( string() )
46
+ } )
47
+
48
+ // Superstruct schema for the complete artifacts configuration with defaults
49
+ const ArtifactsSchema = object( {
50
+ defaults : optional( DefaultsSchema ),
51
+ artifacts : array( union( [ PartialArtifactSchema, PartialGenericArtifactSchema ] ) )
52
+ } )
53
+
54
+ const FlattenedArtifactsSchema = array( assign( union( [ ArtifactSchema, GenericArtifactSchema ] ) ) )
55
+
56
+ export {
57
+ ArtifactTypeEnum,
58
+ ArtifactsSchema,
59
+ FlattenedArtifactsSchema,
60
+ ArtifactSchema,
61
+ DefaultsSchema,
62
+ GenericArtifactSchema,
63
+ }
@@ -0,0 +1,49 @@
1
+ import { validate } from 'superstruct'
2
+
3
+ import { parseYamlFile, warning } from '../Utils.mjs'
4
+
5
+ import { ArtifactTypeEnum, ArtifactsSchema, GenericArtifactSchema } from './Structs.mjs'
6
+
7
+ const getArtifactsSchema = async ( artifactsFilePath = './artifacts.yml' ) => {
8
+ const artifactsConfig = await parseYamlFile( artifactsFilePath )
9
+ const [ error, result ] = validate( artifactsConfig, ArtifactsSchema )
10
+ if ( error ) {
11
+ throw new Error( `Invalid artifacts.yml file: ${error.message}` )
12
+ }
13
+ return result
14
+ }
15
+
16
+ const flattenArtifactsSchema = ( artifactsSchema ) => {
17
+ return artifactsSchema.artifacts.reduce( ( flattenedArtifacts, artifact ) => {
18
+ const mergedArtifact = {
19
+ ...artifactsSchema.defaults,
20
+ ...artifact
21
+ }
22
+
23
+ let schema
24
+ switch ( mergedArtifact.type ) {
25
+ case ArtifactTypeEnum.GENERIC:
26
+ schema = GenericArtifactSchema
27
+ break
28
+ default:
29
+ warning( `Unsupported artifact type "${mergedArtifact.type}"` )
30
+ break
31
+ }
32
+
33
+ if ( !schema ) {
34
+ warning( `Invalid artifact type "${mergedArtifact.type}"` )
35
+ return flattenedArtifacts
36
+ }
37
+
38
+ const [ error, validatedArtifact ] = validate( mergedArtifact, schema )
39
+
40
+ if ( error ) {
41
+ warning( `Invalid artifact "${mergedArtifact.name}": ${error.message}` )
42
+ return flattenedArtifacts
43
+ }
44
+
45
+ return [ ...flattenedArtifacts, validatedArtifact ]
46
+ }, [] )
47
+ }
48
+
49
+ export { getArtifactsSchema, flattenArtifactsSchema }
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+
3
+ async function main() {
4
+ return null
5
+ }
6
+
7
+ main()
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { warning } from '../Utils.mjs'
4
+
5
+ import { getArtifactsSchema, flattenArtifactsSchema } from './Utils.mjs'
6
+ import { ArtifactTypeEnum } from './Structs.mjs'
7
+ import { GenericArtifactPusher } from './ArtifactPusher.mjs'
8
+
9
+ async function main() {
10
+
11
+ // If an argument is provided, use it as the artifacts.yml file path
12
+ const artifactsYmlPath = process.argv[2]
13
+ const artifactPackageName = process.argv[3]
14
+ const artifactPackageVersion = process.argv[4]
15
+
16
+ if ( artifactsYmlPath ) {
17
+ const artifactsSchema = await getArtifactsSchema( artifactsYmlPath )
18
+ const flattenedArtifactsSchema = flattenArtifactsSchema( artifactsSchema )
19
+
20
+ // Open for unexpected matches if we end up re-using the same package name and version for
21
+ // multiple artifacts spread across multiple repositories and/or projects.
22
+ const filteredArtifactSchemas = flattenedArtifactsSchema.filter(
23
+ artifact => artifact.gcp_artifact_registry_package_name === artifactPackageName &&
24
+ artifact.gcp_artifact_registry_package_version === artifactPackageVersion )
25
+
26
+ if ( filteredArtifactSchemas.length === 0 ) {
27
+ console.error( `No artifacts found for ${artifactPackageName} version ${artifactPackageVersion} in ${artifactsYmlPath}` )
28
+ process.exit( 1 )
29
+ }
30
+
31
+ // TODO: Use concurrency to package artifacts and provide summary of results.
32
+ for ( const artifactSchema of filteredArtifactSchemas ) {
33
+ let packager
34
+ switch ( artifactSchema.type ) {
35
+ case ArtifactTypeEnum.GENERIC:
36
+ packager = new GenericArtifactPusher( artifactSchema )
37
+ break
38
+ default:
39
+ warning( `Unsupported artifact type "${artifactSchema.type}"` )
40
+ continue
41
+ }
42
+
43
+ if ( !packager ) {
44
+ warning( `Unsupported artifact type "${artifactSchema.type}"` )
45
+ continue
46
+ }
47
+
48
+ // eslint-disable-next-line no-await-in-loop
49
+ await packager.package()
50
+ }
51
+ }
52
+ }
53
+
54
+ main()
@@ -25,9 +25,13 @@ import commandLineArgs from 'command-line-args'
25
25
  import commandLineUsage from 'command-line-usage'
26
26
  import ms from 'ms'
27
27
  import { lt as semverLt } from 'semver'
28
+ import YAML from 'js-yaml'
28
29
 
29
30
  import { debug, log, shellCmd } from './Utils.mjs'
30
31
 
32
+ const AUTH_TOKEN_REGEX = /_authToken=(.+)/
33
+ const YARNRC_FILE_NAME = `${os.homedir()}/.yarnrc.yml`
34
+
31
35
  const refreshTokenOptions = [ // Use refreshTokenOptions to tie into the Usage statements
32
36
  {
33
37
  name : 'project',
@@ -59,6 +63,12 @@ const refreshTokenOptions = [ // Use refreshTokenOptions to tie into the Usage s
59
63
  default : false,
60
64
  description : '{green display this help screen}',
61
65
  },
66
+ {
67
+ name : 'update-yarnrc',
68
+ type : Boolean,
69
+ defaultValue : true,
70
+ description : '{green updates the npmAuthToken variable of your .yarnrc.yml file (in addition to .npmrc/.npmrc.ro)}'
71
+ }
62
72
  ]
63
73
 
64
74
  const sections = [
@@ -195,11 +205,14 @@ const getGcpSecretCommand = ( secretName ) => {
195
205
 
196
206
  // fetch the contents of the npmrc secret file stored on the specified project
197
207
  let npmrcFile
208
+ let npmAuthToken
198
209
  try {
199
210
  const npmTokenFetcher = getGcpSecretCommand( 'REFRESH_NPM_NPMRC' )
200
211
  debug( { npmTokenFetcher }, '<==Secret Fetcher' )
201
212
 
202
213
  npmrcFile = await shellCmd( npmTokenFetcher )
214
+ npmAuthToken = npmrcFile.match( AUTH_TOKEN_REGEX )?.[1]
215
+ debug( { npmAuthToken }, '<==npmAuthToken' )
203
216
  debug( { npmrcFile }, '<==shellCmd Output npmrc file' )// debug
204
217
  } catch ( err ) {
205
218
  log( chalk.yellow( `\nFailed Command => [${err.escapedCommand}]\n\n` ), chalk.red( err.stderr ) )
@@ -234,6 +247,18 @@ try {
234
247
  fs.copyFileSync( filename, `${filename}-BAK` ) // make a backup for safety
235
248
  }
236
249
  fs.writeFileSync( filename, npmrcFile )
250
+
251
+ if ( args.updateYarnrc && npmAuthToken ) {
252
+ let yarnRcContent = { nodeLinker : 'node-modules' }
253
+ if ( fs.existsSync( YARNRC_FILE_NAME ) ) {
254
+ fs.copyFileSync( YARNRC_FILE_NAME, `${YARNRC_FILE_NAME}-BAK` )
255
+ yarnRcContent = YAML.load( fs.readFileSync( YARNRC_FILE_NAME, 'utf-8' ) )
256
+ debug( { yarnRcContent }, '<==yarnrc content' )
257
+ yarnRcContent.npmAuthToken = npmAuthToken
258
+ }
259
+
260
+ fs.writeFileSync( YARNRC_FILE_NAME, YAML.dump( yarnRcContent ) )
261
+ }
237
262
  }
238
263
  } catch ( fileErr ) {
239
264
  log( fileErr, 'fs failed' )
Binary file
@@ -1,138 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
-
4
- const fs = require('node:fs');
5
- const path = require('node:path');
6
- const chalk = require('chalk');
7
- const cliArgs = require('command-line-args');
8
- const cliHelp = require('command-line-usage');
9
- const npmRegistryFetch = require('npm-registry-fetch');
10
- const semverLt = require('semver/functions/lt');
11
-
12
- /* eslint-disable no-console */
13
-
14
- const optionList = [
15
- // Use optionList to tie into the Usage statements
16
- {
17
- name: 'root',
18
- type: Boolean,
19
- default: false,
20
- description: 'returns the root of the build-tools installation'
21
- }, {
22
- name: 'latest',
23
- type: Boolean,
24
- default: false,
25
- description: 'verifies the latest build-tools are installed'
26
- }, {
27
- name: 'bashfun',
28
- type: Boolean,
29
- default: false,
30
- description: 'source the output to define common bash helper functions'
31
- }, {
32
- name: 'reporoot',
33
- type: Boolean,
34
- default: false,
35
- description: 'the root of the build-tools repo - for finding other config files'
36
- }, {
37
- name: 'help',
38
- type: Boolean,
39
- default: false,
40
- description: 'display this help screen'
41
- }, {
42
- name: 'version',
43
- type: Boolean,
44
- default: false,
45
- description: 'returns the build-tools repo version'
46
- }, {
47
- name: 'verbose',
48
- alias: 'v',
49
- type: Boolean,
50
- default: false,
51
- description: 'emit additional info at run time'
52
- }];
53
- const sections = [{
54
- header: 'A collection of build / support tools for all Leverege code',
55
- content: `README: {green https://bitbucket.org/leverege/build-tools/src/development}
56
- `
57
- }, {
58
- header: 'Options',
59
- optionList
60
- }];
61
- const args = cliArgs(optionList, {
62
- partial: true
63
- });
64
- const help = cliHelp(sections);
65
- const repoRoot = path.dirname(__dirname);
66
- const thisPackage = require(`${repoRoot}/package.json`);
67
- const thisVersion = thisPackage.version;
68
- if (args.root) {
69
- console.log(repoRoot);
70
- process.exit(0);
71
- }
72
- if (args.bashfun) {
73
- console.log(`${repoRoot}/src/bash-funcs`);
74
- process.exit(0);
75
- }
76
- if (args.reporoot) {
77
- console.log(`${repoRoot}`);
78
- process.exit(0);
79
- }
80
- const checkForLatest = async (pkg = '@leverege/build-tools') => {
81
- const getToken = () => {
82
- const tokenFile = `${process.env.HOME}/.npmrc`;
83
- if (!fs.existsSync(tokenFile)) {
84
- console.error(`Cannot find token file ${tokenFile}`);
85
- process.exit(1);
86
- }
87
- try {
88
- const tokenLine = fs.readFileSync(tokenFile).toString();
89
- const tokenRegX = new RegExp('.*registry.npmjs.org\\/:\\w+=+(.*)');
90
- const tokenStr = tokenLine.match(tokenRegX);
91
- if (!tokenStr) {
92
- console.error(`\n***ERROR: malformed npm token in ${tokenFile}\n`);
93
- process.exit(1);
94
- }
95
- return tokenLine.match(tokenRegX)[1];
96
- } catch (err) {
97
- console.error(err);
98
- }
99
- };
100
- try {
101
- const list = await npmRegistryFetch.json(pkg, {
102
- '//registry.npmjs.org/:_authToken': getToken()
103
- });
104
- return list['dist-tags'].latest;
105
- } catch (err) {
106
- console.log(err);
107
- }
108
- };
109
- const checkAndAnnounce = () => {
110
- checkForLatest().then(latestVersion => {
111
- if (semverLt(thisVersion, latestVersion)) {
112
- console.log(chalk.white`
113
- Update available ${chalk.grey(thisVersion)} \u2b62 ${chalk.green(latestVersion)}
114
- Run ${chalk.cyan('npm i -g @leverege/build-tools')} to update
115
- `);
116
- process.exit(1);
117
- } else {
118
- process.exit(0);
119
- }
120
- }).catch(err => {});
121
- };
122
- if (args.latest) {
123
- checkAndAnnounce();
124
- }
125
- if (args.version) {
126
- console.log(`build-tools version ${thisVersion}`);
127
- checkAndAnnounce();
128
- }
129
- if (args.help) {
130
- console.log(help);
131
- process.exit(0);
132
- }
133
-
134
- /* eslint-disable no-underscore-dangle */
135
- if (args._unknown) {
136
- console.log(`\nUnrecognized argument [${args._unknown}] try --help\n`);
137
- process.exit(1);
138
- }