@leverege/build-tools 2.92.0 → 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.92.0",
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()
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
- }