enablement-build-manifest 1.0.2 → 1.0.3
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/_context/build.yml +336 -0
- package/_context/templated.json +48 -0
- package/index.mjs +2 -352
- package/package.json +4 -5
- package/src/cli.mjs +13 -0
- package/src/manifest.mjs +402 -0
- package/src/options.mjs +33 -0
- package/tests/manifest.test.mjs +105 -0
- package/tests/options.test.mjs +40 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
trigger:
|
|
2
|
+
- main
|
|
3
|
+
|
|
4
|
+
parameters:
|
|
5
|
+
- name: publish_ado
|
|
6
|
+
displayName: Publish ADO
|
|
7
|
+
type: boolean
|
|
8
|
+
default: true
|
|
9
|
+
|
|
10
|
+
- name: publish_nexus
|
|
11
|
+
displayName: Publish Nexus
|
|
12
|
+
type: boolean
|
|
13
|
+
default: true
|
|
14
|
+
|
|
15
|
+
pool:
|
|
16
|
+
name: On-Prem Build Agents
|
|
17
|
+
demands:
|
|
18
|
+
- Agent.OS -equals Linux
|
|
19
|
+
|
|
20
|
+
resources:
|
|
21
|
+
repositories:
|
|
22
|
+
- repository: team
|
|
23
|
+
type: git
|
|
24
|
+
name: ECommerce/pipeline-templates
|
|
25
|
+
variables:
|
|
26
|
+
publish_nexus: ${{parameters.publish_nexus}}
|
|
27
|
+
publish_ado: ${{parameters.publish_ado}}
|
|
28
|
+
isMain: $[eq(variables['Build.SourceBranch'], 'refs/heads/main')]
|
|
29
|
+
dockerRoot: deploy
|
|
30
|
+
PIPELINE_NAME: enable-deploy-mono.GH.DEPLOY
|
|
31
|
+
|
|
32
|
+
stages:
|
|
33
|
+
- stage: Version
|
|
34
|
+
jobs:
|
|
35
|
+
- job: CheckForChanges
|
|
36
|
+
steps:
|
|
37
|
+
- checkout: self
|
|
38
|
+
persistCredentials: true
|
|
39
|
+
fetchTags: true,
|
|
40
|
+
fetchDepth: 0,
|
|
41
|
+
clean: true
|
|
42
|
+
- script: |
|
|
43
|
+
REPO=$(echo $(Build.Repository.Name)| cut -d'/' -f 2)
|
|
44
|
+
echo "##vso[task.setvariable variable=repo;isoutput=true;]$REPO"
|
|
45
|
+
BRANCH=$(echo $(Build.SourceBranchName))
|
|
46
|
+
echo $REPO/$BRANCH
|
|
47
|
+
echo "##vso[task.setvariable variable=env;isoutput=true;]DEV"
|
|
48
|
+
|
|
49
|
+
git config --global user.email "icg_it_linux@ingramcontent.com"
|
|
50
|
+
git config --global user.name "CICD"
|
|
51
|
+
|
|
52
|
+
npm i pnpm@10 nx@latest jest -g
|
|
53
|
+
nx graph --file dependencies.json
|
|
54
|
+
git clean -dfX
|
|
55
|
+
npm run release:changed
|
|
56
|
+
displayName: 'Get Changes'
|
|
57
|
+
name: getChanged
|
|
58
|
+
|
|
59
|
+
- stage: Build_Node
|
|
60
|
+
dependsOn: Version
|
|
61
|
+
displayName: Build Node
|
|
62
|
+
jobs:
|
|
63
|
+
- job: BuildChanged
|
|
64
|
+
pool:
|
|
65
|
+
name: 'On-Prem Build Agents'
|
|
66
|
+
demands:
|
|
67
|
+
- Agent.OS -equals Linux
|
|
68
|
+
timeoutInMinutes: 60
|
|
69
|
+
variables:
|
|
70
|
+
changed: $[stageDependencies.Version.CheckForChanges.outputs['getChanged.changed']]
|
|
71
|
+
|
|
72
|
+
steps:
|
|
73
|
+
- checkout: self
|
|
74
|
+
persistCredentials: true
|
|
75
|
+
fetchTags: true,
|
|
76
|
+
fetchDepth: 0,
|
|
77
|
+
clean: true
|
|
78
|
+
- script: |
|
|
79
|
+
echo $(changed)
|
|
80
|
+
cp ./.cicd/ado.npmrc ./.npmrc
|
|
81
|
+
displayName: 'ADO NPM Prepare'
|
|
82
|
+
condition: eq(variables.publish_ado, true)
|
|
83
|
+
- task: npmAuthenticate@0
|
|
84
|
+
inputs:
|
|
85
|
+
customEndpoint: 'ado-npm-icg,ado-npm-lit'
|
|
86
|
+
workingFile: ./.npmrc
|
|
87
|
+
displayName: 'ADO NPM Authentication'
|
|
88
|
+
condition: eq(variables.publish_ado, true)
|
|
89
|
+
- script: |
|
|
90
|
+
mv ./.npmrc ./.npmrc_ado
|
|
91
|
+
displayName: 'ADO NPM Save'
|
|
92
|
+
condition: eq(variables.publish_ado, true)
|
|
93
|
+
|
|
94
|
+
- script: |
|
|
95
|
+
cp ./.cicd/nexus.npmrc ./.npmrc
|
|
96
|
+
displayName: 'Nexus NPM Prepare'
|
|
97
|
+
condition: eq(variables.publish_nexus, true)
|
|
98
|
+
- task: npmAuthenticate@0
|
|
99
|
+
inputs:
|
|
100
|
+
customEndpoint: 'nexus-npm-hosted'
|
|
101
|
+
workingFile: ./.npmrc
|
|
102
|
+
displayName: 'Nexus NPM Authentication'
|
|
103
|
+
condition: eq(variables.publish_nexus, true)
|
|
104
|
+
- script: |
|
|
105
|
+
mv ./.npmrc ./.npmrc_nexus
|
|
106
|
+
displayName: 'Nexus NPM Save'
|
|
107
|
+
condition: eq(variables.publish_nexus, true)
|
|
108
|
+
|
|
109
|
+
- script: |
|
|
110
|
+
npm install -g nx pnpm@10
|
|
111
|
+
nx graph --file dependencies.json
|
|
112
|
+
npm run release:version
|
|
113
|
+
displayName: 'Get Versions'
|
|
114
|
+
name: getVersions
|
|
115
|
+
|
|
116
|
+
- script: |
|
|
117
|
+
cp ./.cicd/nexus-group.npmrc ./.npmrc
|
|
118
|
+
#rm -f pnpm-lock.yaml
|
|
119
|
+
pnpm i
|
|
120
|
+
name: INSTALL
|
|
121
|
+
|
|
122
|
+
- script: |
|
|
123
|
+
export NX_DISABLE_DB=true
|
|
124
|
+
pnpm run build
|
|
125
|
+
name: BUILD
|
|
126
|
+
|
|
127
|
+
- script: |
|
|
128
|
+
pnpm test
|
|
129
|
+
displayName: 'TEST'
|
|
130
|
+
name: tester
|
|
131
|
+
|
|
132
|
+
# - template: sonarqube-prepare.yml@team
|
|
133
|
+
# parameters:
|
|
134
|
+
# scannerMode: "other"
|
|
135
|
+
|
|
136
|
+
####################PACKAGES
|
|
137
|
+
{PACKAGES}
|
|
138
|
+
####################PACKAGES
|
|
139
|
+
|
|
140
|
+
####################API
|
|
141
|
+
{API}
|
|
142
|
+
####################API
|
|
143
|
+
|
|
144
|
+
####################UX
|
|
145
|
+
{UX}
|
|
146
|
+
####################UX
|
|
147
|
+
|
|
148
|
+
- stage: Build_Python
|
|
149
|
+
dependsOn: Version
|
|
150
|
+
displayName: Build Python
|
|
151
|
+
jobs:
|
|
152
|
+
- job: BuildChanged
|
|
153
|
+
pool:
|
|
154
|
+
name: 'On-Prem Build Agents'
|
|
155
|
+
demands:
|
|
156
|
+
- Agent.OS -equals Linux
|
|
157
|
+
timeoutInMinutes: 60
|
|
158
|
+
variables:
|
|
159
|
+
changed: $[stageDependencies.Version.CheckForChanges.outputs['getChanged.changed']]
|
|
160
|
+
|
|
161
|
+
steps:
|
|
162
|
+
- checkout: self
|
|
163
|
+
persistCredentials: true
|
|
164
|
+
fetchTags: true,
|
|
165
|
+
fetchDepth: 0,
|
|
166
|
+
clean: true
|
|
167
|
+
|
|
168
|
+
- script: |
|
|
169
|
+
npm install -g nx pnpm@10
|
|
170
|
+
nx graph --file dependencies.json
|
|
171
|
+
npm run release:version
|
|
172
|
+
displayName: 'Get Versions'
|
|
173
|
+
name: getVersions
|
|
174
|
+
|
|
175
|
+
# - script: |
|
|
176
|
+
# export NX_DISABLE_DB=true
|
|
177
|
+
# pnpm run build:python
|
|
178
|
+
# name: BUILD
|
|
179
|
+
|
|
180
|
+
# - script: |
|
|
181
|
+
# pnpm test:python
|
|
182
|
+
# displayName: 'TEST'
|
|
183
|
+
# name: tester
|
|
184
|
+
|
|
185
|
+
####################PYTHON_PACKAGES
|
|
186
|
+
{PYTHON_PACKAGES}
|
|
187
|
+
####################PYTHON_PACKAGES
|
|
188
|
+
|
|
189
|
+
- task: CopyFiles@2
|
|
190
|
+
displayName: 'Copy Lock Files to Staging'
|
|
191
|
+
inputs:
|
|
192
|
+
SourceFolder: '$(System.DefaultWorkingDirectory)'
|
|
193
|
+
Contents: |
|
|
194
|
+
**/poetry.lock
|
|
195
|
+
**/requirements.txt
|
|
196
|
+
**/pyproject.toml
|
|
197
|
+
TargetFolder: '$(Build.ArtifactStagingDirectory)'
|
|
198
|
+
|
|
199
|
+
- task: PublishPipelineArtifact@1
|
|
200
|
+
displayName: 'Publish Python Lock Files'
|
|
201
|
+
inputs:
|
|
202
|
+
targetPath: '$(Build.ArtifactStagingDirectory)'
|
|
203
|
+
artifactName: 'python-lock-files'
|
|
204
|
+
publishLocation: 'pipeline'
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
- stage: Build_DotNet
|
|
208
|
+
dependsOn: Version
|
|
209
|
+
displayName: Build DotNet
|
|
210
|
+
jobs:
|
|
211
|
+
- job: BuildChanged
|
|
212
|
+
pool:
|
|
213
|
+
name: 'On-Prem Build Agents'
|
|
214
|
+
demands:
|
|
215
|
+
- Agent.OS -equals Linux
|
|
216
|
+
timeoutInMinutes: 60
|
|
217
|
+
variables:
|
|
218
|
+
changed: $[stageDependencies.Version.CheckForChanges.outputs['getChanged.changed']]
|
|
219
|
+
|
|
220
|
+
steps:
|
|
221
|
+
- checkout: self
|
|
222
|
+
persistCredentials: true
|
|
223
|
+
fetchTags: true,
|
|
224
|
+
fetchDepth: 0,
|
|
225
|
+
clean: true
|
|
226
|
+
|
|
227
|
+
- script: |
|
|
228
|
+
npm install -g nx pnpm@10
|
|
229
|
+
nx graph --file dependencies.json
|
|
230
|
+
npm run release:version
|
|
231
|
+
displayName: 'Get Versions'
|
|
232
|
+
name: getVersions
|
|
233
|
+
|
|
234
|
+
####################DOTNET_PACKAGES
|
|
235
|
+
{DOTNET_PACKAGES}
|
|
236
|
+
####################DOTNET_PACKAGES
|
|
237
|
+
|
|
238
|
+
- stage: Build_Containers
|
|
239
|
+
dependsOn:
|
|
240
|
+
- Version
|
|
241
|
+
- Build_Python
|
|
242
|
+
- Build_Node
|
|
243
|
+
- Build_DotNet
|
|
244
|
+
displayName: Build Containers
|
|
245
|
+
jobs:
|
|
246
|
+
- job: BuildChanged
|
|
247
|
+
pool:
|
|
248
|
+
name: 'On-Prem Build Agents'
|
|
249
|
+
demands:
|
|
250
|
+
- Agent.OS -equals Linux
|
|
251
|
+
timeoutInMinutes: 60
|
|
252
|
+
variables:
|
|
253
|
+
changed: $[stageDependencies.Version.CheckForChanges.outputs['getChanged.changed']]
|
|
254
|
+
|
|
255
|
+
steps:
|
|
256
|
+
- checkout: self
|
|
257
|
+
persistCredentials: true
|
|
258
|
+
fetchTags: true,
|
|
259
|
+
fetchDepth: 0,
|
|
260
|
+
clean: true
|
|
261
|
+
|
|
262
|
+
- script: |
|
|
263
|
+
npm install -g nx pnpm@10
|
|
264
|
+
nx graph --file dependencies.json
|
|
265
|
+
npm run release:version
|
|
266
|
+
displayName: 'Get Versions'
|
|
267
|
+
name: getVersions
|
|
268
|
+
|
|
269
|
+
# - script: |
|
|
270
|
+
# pnpm test
|
|
271
|
+
# displayName: 'TEST'
|
|
272
|
+
# name: tester
|
|
273
|
+
|
|
274
|
+
####################CONTAINERS
|
|
275
|
+
{CONTAINERS}
|
|
276
|
+
####################CONTAINERS
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
- stage: Release
|
|
280
|
+
dependsOn:
|
|
281
|
+
- Version
|
|
282
|
+
- Build_Containers
|
|
283
|
+
- Build_Python
|
|
284
|
+
- Build_Node
|
|
285
|
+
- Build_DotNet
|
|
286
|
+
|
|
287
|
+
variables:
|
|
288
|
+
VERSION: $[ stageDependencies.Version.CheckForChanges.outputs['getChanged.tag'] ]
|
|
289
|
+
REPO: $[ stageDependencies.Version.CheckForChanges.outputs['getChanged.repo'] ]
|
|
290
|
+
ENV: $[ stageDependencies.Version.CheckForChanges.outputs['getChanged.env'] ]
|
|
291
|
+
BRANCH: $(Build.SourceBranchName)
|
|
292
|
+
jobs:
|
|
293
|
+
- job: Push
|
|
294
|
+
condition: and(succeeded(), eq(variables.isMain, true))
|
|
295
|
+
steps:
|
|
296
|
+
- checkout: self
|
|
297
|
+
persistCredentials: true
|
|
298
|
+
fetchTags: true,
|
|
299
|
+
fetchDepth: 0,
|
|
300
|
+
clean: true
|
|
301
|
+
- script: |
|
|
302
|
+
git config --global user.email "icg_it_linux@ingramcontent.com"
|
|
303
|
+
git config --global user.name "CICD"
|
|
304
|
+
|
|
305
|
+
SHA=$(git rev-parse HEAD)
|
|
306
|
+
working=working$SHA
|
|
307
|
+
git checkout -b $working
|
|
308
|
+
git branch -D $BRANCH &>/dev/null || true
|
|
309
|
+
git checkout $BRANCH
|
|
310
|
+
git branch -D $working &>/dev/null || true
|
|
311
|
+
|
|
312
|
+
git clean -dfX
|
|
313
|
+
displayName: Prepare
|
|
314
|
+
|
|
315
|
+
- task: DownloadPipelineArtifact@2
|
|
316
|
+
displayName: 'Restore Python Files'
|
|
317
|
+
inputs:
|
|
318
|
+
artifactName: 'python-lock-files'
|
|
319
|
+
targetPath: '$(System.DefaultWorkingDirectory)'
|
|
320
|
+
|
|
321
|
+
- script: |
|
|
322
|
+
npm run release:version
|
|
323
|
+
npm run release:finalize
|
|
324
|
+
npx enablement-build-version-repo --version --debug --save $VERSION
|
|
325
|
+
displayName: Version
|
|
326
|
+
|
|
327
|
+
- script: |
|
|
328
|
+
git commit -am "chore(release): $VERSION" -m "[skip ci]" --no-verify
|
|
329
|
+
git push
|
|
330
|
+
git push --tags
|
|
331
|
+
displayName: Success
|
|
332
|
+
|
|
333
|
+
- template: steps/deploy-ado-pipeline.yml@team
|
|
334
|
+
parameters:
|
|
335
|
+
pipelineName: $(PIPELINE_NAME)
|
|
336
|
+
branch: $(BRANCH)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"build.yml": {
|
|
3
|
+
"CONTAINERS": {
|
|
4
|
+
"rootPath": "../../containers",
|
|
5
|
+
"template": "build_docker_package.yml"
|
|
6
|
+
},
|
|
7
|
+
"CONTAINERS_CHECKMARX": {
|
|
8
|
+
"rootPath": "../../containers",
|
|
9
|
+
"template": "build_scan.yml"
|
|
10
|
+
},
|
|
11
|
+
"API": {
|
|
12
|
+
"rootPath": "../../api",
|
|
13
|
+
"template": "build_node_package.yml"
|
|
14
|
+
},
|
|
15
|
+
"PACKAGES": {
|
|
16
|
+
"rootPath": "../../packages",
|
|
17
|
+
"template": "build_node_component.yml"
|
|
18
|
+
},
|
|
19
|
+
"UX": {
|
|
20
|
+
"rootPath": "../../ux",
|
|
21
|
+
"template": "build_node_package.yml"
|
|
22
|
+
},
|
|
23
|
+
"CHECKMARX_UX": {
|
|
24
|
+
"rootPath": "../../ux",
|
|
25
|
+
"template": "build_scan_node.yml"
|
|
26
|
+
},
|
|
27
|
+
"CHECKMARX_API": {
|
|
28
|
+
"rootPath": "../../api",
|
|
29
|
+
"template": "build_scan_node.yml"
|
|
30
|
+
},
|
|
31
|
+
"PYTHON_PACKAGES": {
|
|
32
|
+
"rootPath":"../../python",
|
|
33
|
+
"template":"build_python_package.yml"
|
|
34
|
+
},
|
|
35
|
+
"PYTHON_CHECKMARX": {
|
|
36
|
+
"rootPath":"../../python",
|
|
37
|
+
"template":"build_scan.yml"
|
|
38
|
+
},
|
|
39
|
+
"DOTNET_PACKAGES": {
|
|
40
|
+
"rootPath":"../../dotnet",
|
|
41
|
+
"template":"build_dotnet_package.yml"
|
|
42
|
+
},
|
|
43
|
+
"DOTNET_CHECKMARX": {
|
|
44
|
+
"rootPath":"../../dotnet",
|
|
45
|
+
"template":"build_scan.yml"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
package/index.mjs
CHANGED
|
@@ -1,355 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import { globbySync } from 'globby';
|
|
5
|
-
import YAML from 'yaml'
|
|
6
|
-
import path from 'path';
|
|
3
|
+
import { runCli } from './src/cli.mjs';
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (!options.platform) {
|
|
11
|
-
options.platform = (fs.existsSync('.github/templates/templated.json') || fs.existsSync('.github/manifest.json')) ? 'github' : 'ado';
|
|
12
|
-
}
|
|
13
|
-
const configDir = options.platform === 'github' ? '.github' : '.cicd';
|
|
14
|
-
|
|
15
|
-
// Find all package.json files, excluding node_modules
|
|
16
|
-
const packageJsonPaths = globbySync('*/**/package.json', {
|
|
17
|
-
ignore: ['**/node_modules/**', '**/tests/**'],
|
|
18
|
-
cwd: process.cwd(),
|
|
19
|
-
absolute: true
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
const nameToPath = {};
|
|
23
|
-
|
|
24
|
-
const versions = {};
|
|
25
|
-
const dependencies = {};
|
|
26
|
-
const dependents = {};
|
|
27
|
-
const cicd = {};
|
|
28
|
-
const pipelines = {};
|
|
29
|
-
|
|
30
|
-
const packageData = {};
|
|
31
|
-
const stage2Packages = {};
|
|
32
|
-
|
|
33
|
-
// First pass: collect all local package names and versions
|
|
34
|
-
packageJsonPaths.forEach(pkgPath => {
|
|
35
|
-
const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
36
|
-
packageData[pkgPath] = pkgJson;
|
|
37
|
-
if (pkgJson.name && pkgJson.version) {
|
|
38
|
-
nameToPath[pkgJson.name] = pkgPath;
|
|
39
|
-
|
|
40
|
-
// create map of stage to packages
|
|
41
|
-
const stageName = path.basename(path.dirname(path.dirname(pkgPath)));
|
|
42
|
-
if(!stage2Packages[stageName]) stage2Packages[stageName] = [];
|
|
43
|
-
stage2Packages[stageName].push(pkgJson.name);
|
|
44
|
-
|
|
45
|
-
versions[pkgJson.name] = pkgJson.version;
|
|
46
|
-
dependents[pkgJson.name] = []; // Initialize reverse dependency map
|
|
47
|
-
const folderName = path.basename(path.dirname(pkgPath));
|
|
48
|
-
const packageFolder = path.basename(path.dirname(path.dirname(pkgPath)));
|
|
49
|
-
if (pkgJson['.cicd']) {
|
|
50
|
-
const cicdEntry = { ...pkgJson['.cicd'] };
|
|
51
|
-
delete cicdEntry.templates;
|
|
52
|
-
cicdEntry.folderName = folderName;
|
|
53
|
-
cicdEntry.packageFolder = packageFolder;
|
|
54
|
-
cicd[pkgJson.name] = cicdEntry;
|
|
55
|
-
}
|
|
56
|
-
// Always ensure folderName/packageFolder are present if cicd entry exists
|
|
57
|
-
if (cicd[pkgJson.name]) {
|
|
58
|
-
if (!cicd[pkgJson.name].folderName) cicd[pkgJson.name].folderName = folderName;
|
|
59
|
-
if (!cicd[pkgJson.name].packageFolder) cicd[pkgJson.name].packageFolder = packageFolder;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
if(options.debug)
|
|
65
|
-
console.log(`Discovered ${Object.keys(nameToPath).length} local packages.`);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// Second pass: build dependency tree
|
|
69
|
-
Object.entries(nameToPath).forEach(([pkgName, pkgPath]) => {
|
|
70
|
-
const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
71
|
-
const allDeps = {
|
|
72
|
-
...pkgJson.dependencies,
|
|
73
|
-
...pkgJson.devDependencies
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const localDeps = Object.keys(allDeps || {}).filter(dep => nameToPath.hasOwnProperty(dep));
|
|
77
|
-
dependencies[pkgName] = localDeps;
|
|
78
|
-
|
|
79
|
-
// Populate reverse dependencies
|
|
80
|
-
localDeps.forEach(dep => {
|
|
81
|
-
if (!dependents[dep]) {
|
|
82
|
-
dependents[dep] = [];
|
|
83
|
-
}
|
|
84
|
-
dependents[dep].push(pkgName);
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// now find all packages that do not have a .cicd entry
|
|
89
|
-
Object.entries(nameToPath).forEach(([pkgName, pkgPath]) => {
|
|
90
|
-
if (!cicd[pkgName]) {
|
|
91
|
-
|
|
92
|
-
var pkg = packageData[pkgPath];
|
|
93
|
-
if (pkg['main'] || pkg['module']) {
|
|
94
|
-
cicd[pkgName] = {
|
|
95
|
-
output: "npm",
|
|
96
|
-
language: 'JavaScript',
|
|
97
|
-
tier: 'component'
|
|
98
|
-
}
|
|
99
|
-
if(pkg["bin"]) {
|
|
100
|
-
cicd[pkgName]["packageType"] = "cli";
|
|
101
|
-
cicd[pkgName]["tier"] = "tooling";
|
|
102
|
-
}
|
|
103
|
-
return;
|
|
104
|
-
} else {
|
|
105
|
-
if ((pkg['devDependencies'] && pkg['devDependencies']['moleculer']) || (pkg['dependencies'] && pkg['dependencies']['moleculer'])) {
|
|
106
|
-
cicd[pkgName] = {
|
|
107
|
-
"output": "npm",
|
|
108
|
-
"language": "node",
|
|
109
|
-
"packageType": "moleculer",
|
|
110
|
-
"tier": "api"
|
|
111
|
-
}
|
|
112
|
-
return;
|
|
113
|
-
} else if ((pkg['devDependencies'] && pkg['devDependencies']['react']) || (pkg['dependencies'] && pkg['dependencies']['react'])) {
|
|
114
|
-
cicd[pkgName] = {
|
|
115
|
-
"output": "npm",
|
|
116
|
-
"language": "JavaScript",
|
|
117
|
-
"packageType": "ux",
|
|
118
|
-
"tier": "ux"
|
|
119
|
-
}
|
|
120
|
-
return;
|
|
121
|
-
} else {
|
|
122
|
-
// ok it is not node
|
|
123
|
-
var folderName = path.dirname(pkgPath);
|
|
124
|
-
const csproj = globbySync('*.csproj', { cwd: folderName, absolute: true });
|
|
125
|
-
const toml = globbySync('pyproject.toml', { cwd: folderName, absolute: true });
|
|
126
|
-
const docker = globbySync('Dockerfile', { cwd: folderName, absolute: true });
|
|
127
|
-
|
|
128
|
-
if (csproj.length > 0) {
|
|
129
|
-
cicd[pkgName] = {
|
|
130
|
-
"output": "nuget",
|
|
131
|
-
"language": "C#",
|
|
132
|
-
"packageType": "dotnet",
|
|
133
|
-
"tier": "api"
|
|
134
|
-
}
|
|
135
|
-
return;
|
|
136
|
-
} else if (toml.length > 0) {
|
|
137
|
-
cicd[pkgName] = {
|
|
138
|
-
"output": "pypi",
|
|
139
|
-
"language": "Python",
|
|
140
|
-
"tier": "api"
|
|
141
|
-
}
|
|
142
|
-
if (pkgName.toLocaleLowerCase().endsWith("api")) cicd[pkgName]["packageType"] = "pypi";
|
|
143
|
-
return;
|
|
144
|
-
} else if (docker.length > 0 && (pkgPath.indexOf("image") >= 0 || pkgPath.indexOf("container") >= 0)) {
|
|
145
|
-
cicd[pkgName] = {
|
|
146
|
-
"output": "docker",
|
|
147
|
-
"language": "Dockerfile",
|
|
148
|
-
"tier": "infra"
|
|
149
|
-
}
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// Ensure folderName and packageFolder are set on every cicd entry
|
|
158
|
-
Object.entries(nameToPath).forEach(([pkgName, pkgPath]) => {
|
|
159
|
-
if (cicd[pkgName]) {
|
|
160
|
-
if (!cicd[pkgName].folderName)
|
|
161
|
-
cicd[pkgName].folderName = path.basename(path.dirname(pkgPath));
|
|
162
|
-
if (!cicd[pkgName].packageFolder)
|
|
163
|
-
cicd[pkgName].packageFolder = path.basename(path.dirname(path.dirname(pkgPath)));
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// now find all packages where templates are missing
|
|
168
|
-
// first load the templated.json
|
|
169
|
-
let templatedConfig = {};
|
|
170
|
-
if (fs.existsSync(`./${configDir}/templates/templated.json`)) {
|
|
171
|
-
let data = JSON.parse(fs.readFileSync(`./${configDir}/templates/templated.json`, 'utf-8'));
|
|
172
|
-
let innerNames ={};
|
|
173
|
-
for (const outerKey in data) {
|
|
174
|
-
const innerObj = data[outerKey];
|
|
175
|
-
const innerKeys = Object.keys(innerObj);
|
|
176
|
-
innerKeys.forEach(innerKey =>{
|
|
177
|
-
let { rootPath, template } = innerObj[innerKey];
|
|
178
|
-
rootPath = rootPath.replace(/\/+$/,''); // remove trailing slash
|
|
179
|
-
rootPath = rootPath.replace(/\.\.\/+/g, ''); // remove leading ../
|
|
180
|
-
|
|
181
|
-
innerNames[innerKey] = rootPath;
|
|
182
|
-
templatedConfig[rootPath] = templatedConfig[rootPath] || [];
|
|
183
|
-
templatedConfig[rootPath].push(template);
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
for (const outerKey in data) {
|
|
188
|
-
if(!pipelines[outerKey]) {
|
|
189
|
-
let pipeline = pipelines[outerKey] = [];
|
|
190
|
-
|
|
191
|
-
let tpl = fs.readFileSync(`./${configDir}/templates/${outerKey}`, 'utf-8');
|
|
192
|
-
|
|
193
|
-
let i = 0;
|
|
194
|
-
for (const innerKey of Object.keys(innerNames)) {
|
|
195
|
-
const regex = new RegExp(`\{\s*${innerKey.toUpperCase()}\s*\}`, 'g');
|
|
196
|
-
if(i==0) tpl = tpl.replace(regex, ` blocks:
|
|
197
|
-
- ${innerKey.toUpperCase()}`);
|
|
198
|
-
else tpl = tpl.replace(regex, ` - ${innerKey.toUpperCase()}`);
|
|
199
|
-
i++;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
var yaml = YAML.parse(tpl);
|
|
203
|
-
for(const s of Object.keys(yaml.stages))
|
|
204
|
-
{
|
|
205
|
-
let stage = yaml.stages[s];
|
|
206
|
-
|
|
207
|
-
let blocks = stage.blocks;
|
|
208
|
-
if(blocks) {
|
|
209
|
-
for(const b of blocks) {
|
|
210
|
-
let pgkList= stage2Packages[innerNames[b]];
|
|
211
|
-
if(pgkList) stage.packages = pgkList;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
delete stage.blocks;
|
|
216
|
-
delete stage.jobs;
|
|
217
|
-
delete stage.variables;
|
|
218
|
-
pipeline.push(stage);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const templates = globbySync(`./${configDir}/templates/*.yml`);
|
|
225
|
-
const withUnderscore = templates
|
|
226
|
-
.filter(file => file.includes('_'))
|
|
227
|
-
.map(file => path.basename(file));
|
|
228
|
-
|
|
229
|
-
const codeScan = withUnderscore.filter(file => file.includes('scan') || file.includes('check'));
|
|
230
|
-
const build = withUnderscore.filter(file => !file.includes('scan') && !file.includes('check'));
|
|
231
|
-
|
|
232
|
-
// Load existing manifest to preserve any already-set templated values
|
|
233
|
-
const manifestPath = `./${configDir}/manifest.json`;
|
|
234
|
-
const existingManifest = fs.existsSync(manifestPath)
|
|
235
|
-
? JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
|
|
236
|
-
: {};
|
|
237
|
-
const existingTemplated = existingManifest.templated || {};
|
|
238
|
-
const existingPackages = (existingTemplated["build.yml"] || {})["PACKAGES"] || {};
|
|
239
|
-
const existingCicd = existingManifest.cicd || {};
|
|
240
|
-
|
|
241
|
-
const templated = {
|
|
242
|
-
"build.yml": {
|
|
243
|
-
"PACKAGES": {}
|
|
244
|
-
}
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
Object.entries(nameToPath).forEach(([pkgName, pkgPath]) => {
|
|
248
|
-
if (cicd[pkgName]) {
|
|
249
|
-
// Preserve any existing templated assignment for this package
|
|
250
|
-
if (existingPackages[pkgName] !== undefined) {
|
|
251
|
-
templated["build.yml"]["PACKAGES"][pkgName] = existingPackages[pkgName];
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
const pkgDir = path.basename(path.dirname(path.dirname(pkgPath)));
|
|
255
|
-
if(templatedConfig[pkgDir]) {
|
|
256
|
-
templated["build.yml"]["PACKAGES"][pkgName] = templatedConfig[pkgDir].length === 1
|
|
257
|
-
? templatedConfig[pkgDir][0]
|
|
258
|
-
: templatedConfig[pkgDir];
|
|
259
|
-
} else {
|
|
260
|
-
let lookfor="node";
|
|
261
|
-
if(cicd[pkgName]["output"]=="pypi") lookfor = "python";
|
|
262
|
-
if(cicd[pkgName]["output"]=="nuget") lookfor = "dotnet";
|
|
263
|
-
if(cicd[pkgName]["output"]=="docker") lookfor = "docker";
|
|
264
|
-
|
|
265
|
-
let templates=[];
|
|
266
|
-
build.forEach(file => {
|
|
267
|
-
if (file.includes(lookfor) ) {
|
|
268
|
-
templates.push(file);
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
let found = false;
|
|
272
|
-
codeScan.forEach(file => {
|
|
273
|
-
if (file.includes(lookfor) ) {
|
|
274
|
-
templates.push(file);
|
|
275
|
-
found = true;
|
|
276
|
-
}
|
|
277
|
-
});
|
|
278
|
-
if(!found && templates.length>0) templates.push("build_scan.yml");
|
|
279
|
-
templated["build.yml"]["PACKAGES"][pkgName] = templates.length === 1 ? templates[0] : templates;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
// Merge in any pre-existing cicd fields (hash, folderName, packageFolder set by release:finalize)
|
|
285
|
-
Object.keys(existingCicd).forEach(pkgName => {
|
|
286
|
-
if (cicd[pkgName]) {
|
|
287
|
-
// Preserve fields from the existing entry that we don't regenerate
|
|
288
|
-
if (existingCicd[pkgName].hash) cicd[pkgName].hash = existingCicd[pkgName].hash;
|
|
289
|
-
if (existingCicd[pkgName].version) cicd[pkgName].version = existingCicd[pkgName].version;
|
|
290
|
-
} else {
|
|
291
|
-
// If package was in cicd before but not now, preserve entire entry
|
|
292
|
-
cicd[pkgName] = existingCicd[pkgName];
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
// Final output structure
|
|
297
|
-
const output = {
|
|
298
|
-
versions,
|
|
299
|
-
dependencies,
|
|
300
|
-
dependents,
|
|
301
|
-
pipelines,
|
|
302
|
-
templated,
|
|
303
|
-
cicd
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
const outputJson = JSON.stringify(output, null, 2)
|
|
307
|
-
if(options.save) fs.writeFileSync(`./${configDir}/manifest.json`, outputJson, 'utf-8');
|
|
308
|
-
return outputJson;
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
let options = {
|
|
312
|
-
debug:false,
|
|
313
|
-
platform:null,
|
|
314
|
-
hashExcludeFolders:['node_modules', 'coverage', 'dist'],
|
|
315
|
-
hashExcludeFiles:['.npmrc','CHANGELOG.md','README.md']
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
// if (process.argv.length === 2) {
|
|
319
|
-
// console.error('Expected at least one argument!');
|
|
320
|
-
// process.exit(1);
|
|
321
|
-
// } else {
|
|
322
|
-
let argv = process.argv;
|
|
323
|
-
for(let i=2;i<argv.length;i++) {
|
|
324
|
-
if(argv[i]==="--save") options.save=true;
|
|
325
|
-
else if(argv[i]==="--debug") options.debug=true;
|
|
326
|
-
else if(argv[i]==="--changed") options.changed=true;
|
|
327
|
-
else if(argv[i]==="--hash") options.hash=true;
|
|
328
|
-
else if(argv[i]==="--platform") { options.platform = argv[i+1]; i++; }
|
|
329
|
-
else if(argv[i]==="--hashExcludeFolders" || argv[i]==="--hashExcludeFiles") {
|
|
330
|
-
let name = argv[i].substring(2);
|
|
331
|
-
options[name] = argv[i+1].split(',');
|
|
332
|
-
i++;
|
|
333
|
-
} else
|
|
334
|
-
if(argv[i].substring(0,2)==="--") {
|
|
335
|
-
let name = argv[i].substring(2);
|
|
336
|
-
if(options[name]!==undefined) {
|
|
337
|
-
options[name] = argv[i+1];
|
|
338
|
-
i++;
|
|
339
|
-
} else {
|
|
340
|
-
console.error(`Expected a known option, got ${argv[i]}`);
|
|
341
|
-
process.exit(1);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
//}
|
|
346
|
-
|
|
347
|
-
(async () => {
|
|
348
|
-
try {
|
|
349
|
-
const text = await main(options);
|
|
350
|
-
console.log(text);
|
|
351
|
-
} catch (e) {
|
|
352
|
-
console.log(e);
|
|
353
|
-
process.exit(1);
|
|
354
|
-
}
|
|
355
|
-
})();
|
|
5
|
+
await runCli();
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "enablement-build-manifest",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "This builds a dependency tree.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./index.mjs",
|
|
7
|
+
"bin": "./index.mjs",
|
|
7
8
|
"license": "MIT",
|
|
8
9
|
"author": "Contributors",
|
|
9
10
|
"dependencies": {
|
|
@@ -11,9 +12,7 @@
|
|
|
11
12
|
"yaml": "^2.8.1"
|
|
12
13
|
},
|
|
13
14
|
"scripts": {
|
|
14
|
-
"start": "node index.mjs"
|
|
15
|
-
|
|
16
|
-
"bin": {
|
|
17
|
-
"enablement-build-manifest": "./index.mjs"
|
|
15
|
+
"start": "node index.mjs",
|
|
16
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules jest tests/*.test.mjs"
|
|
18
17
|
}
|
|
19
18
|
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { generateManifest } from './manifest.mjs';
|
|
2
|
+
import { parseCliArgs } from './options.mjs';
|
|
3
|
+
|
|
4
|
+
export const runCli = async (argv = process.argv, log = console.log, exit = process.exit) => {
|
|
5
|
+
try {
|
|
6
|
+
const options = parseCliArgs(argv);
|
|
7
|
+
const text = await generateManifest(options);
|
|
8
|
+
log(text);
|
|
9
|
+
} catch (e) {
|
|
10
|
+
log(e);
|
|
11
|
+
exit(1);
|
|
12
|
+
}
|
|
13
|
+
};
|
package/src/manifest.mjs
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { globbySync } from 'globby';
|
|
3
|
+
import YAML from 'yaml'
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
export const generateManifest = async (options) => {
|
|
7
|
+
|
|
8
|
+
if (!options.platform) {
|
|
9
|
+
options.platform = (fs.existsSync('.github/templates/templated.json') || fs.existsSync('.github/manifest.json')) ? 'github' : 'ado';
|
|
10
|
+
}
|
|
11
|
+
const configDir = options.platform === 'github' ? '.github' : '.cicd';
|
|
12
|
+
|
|
13
|
+
// Find all package.json files, excluding node_modules
|
|
14
|
+
const packageJsonPaths = globbySync('*/**/package.json', {
|
|
15
|
+
ignore: ['**/node_modules/**', '**/tests/**'],
|
|
16
|
+
cwd: process.cwd(),
|
|
17
|
+
absolute: true
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const nameToPath = {};
|
|
21
|
+
|
|
22
|
+
const versions = {};
|
|
23
|
+
const dependencies = {};
|
|
24
|
+
const dependents = {};
|
|
25
|
+
const cicd = {};
|
|
26
|
+
const pipelines = {};
|
|
27
|
+
|
|
28
|
+
const packageData = {};
|
|
29
|
+
const stage2Packages = {};
|
|
30
|
+
|
|
31
|
+
// First pass: collect all local package names and versions
|
|
32
|
+
packageJsonPaths.forEach(pkgPath => {
|
|
33
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
34
|
+
packageData[pkgPath] = pkgJson;
|
|
35
|
+
if (pkgJson.name && pkgJson.version) {
|
|
36
|
+
nameToPath[pkgJson.name] = pkgPath;
|
|
37
|
+
|
|
38
|
+
// create map of stage to packages
|
|
39
|
+
const stageName = path.basename(path.dirname(path.dirname(pkgPath)));
|
|
40
|
+
if(!stage2Packages[stageName]) stage2Packages[stageName] = [];
|
|
41
|
+
stage2Packages[stageName].push(pkgJson.name);
|
|
42
|
+
|
|
43
|
+
versions[pkgJson.name] = pkgJson.version;
|
|
44
|
+
dependents[pkgJson.name] = []; // Initialize reverse dependency map
|
|
45
|
+
const folderName = path.basename(path.dirname(pkgPath));
|
|
46
|
+
const packageFolder = path.basename(path.dirname(path.dirname(pkgPath)));
|
|
47
|
+
if (pkgJson['.cicd']) {
|
|
48
|
+
const cicdEntry = { ...pkgJson['.cicd'] };
|
|
49
|
+
delete cicdEntry.templates;
|
|
50
|
+
cicdEntry.folderName = folderName;
|
|
51
|
+
cicdEntry.packageFolder = packageFolder;
|
|
52
|
+
cicd[pkgJson.name] = cicdEntry;
|
|
53
|
+
}
|
|
54
|
+
// Always ensure folderName/packageFolder are present if cicd entry exists
|
|
55
|
+
if (cicd[pkgJson.name]) {
|
|
56
|
+
if (!cicd[pkgJson.name].folderName) cicd[pkgJson.name].folderName = folderName;
|
|
57
|
+
if (!cicd[pkgJson.name].packageFolder) cicd[pkgJson.name].packageFolder = packageFolder;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if(options.debug)
|
|
63
|
+
console.log(`Discovered ${Object.keys(nameToPath).length} local packages.`);
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
// Second pass: build dependency tree
|
|
67
|
+
Object.entries(nameToPath).forEach(([pkgName, pkgPath]) => {
|
|
68
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
69
|
+
const allDeps = {
|
|
70
|
+
...pkgJson.dependencies,
|
|
71
|
+
...pkgJson.devDependencies
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const localDeps = Object.keys(allDeps || {}).filter(dep => nameToPath.hasOwnProperty(dep));
|
|
75
|
+
dependencies[pkgName] = localDeps;
|
|
76
|
+
|
|
77
|
+
// Populate reverse dependencies
|
|
78
|
+
localDeps.forEach(dep => {
|
|
79
|
+
if (!dependents[dep]) {
|
|
80
|
+
dependents[dep] = [];
|
|
81
|
+
}
|
|
82
|
+
dependents[dep].push(pkgName);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// now find all packages that do not have a .cicd entry
|
|
87
|
+
Object.entries(nameToPath).forEach(([pkgName, pkgPath]) => {
|
|
88
|
+
if (!cicd[pkgName]) {
|
|
89
|
+
|
|
90
|
+
var pkg = packageData[pkgPath];
|
|
91
|
+
if (pkg['main'] || pkg['module']) {
|
|
92
|
+
cicd[pkgName] = {
|
|
93
|
+
output: "npm",
|
|
94
|
+
language: 'JavaScript',
|
|
95
|
+
tier: 'component'
|
|
96
|
+
}
|
|
97
|
+
if(pkg["bin"]) {
|
|
98
|
+
cicd[pkgName]["packageType"] = "cli";
|
|
99
|
+
cicd[pkgName]["tier"] = "tooling";
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
} else {
|
|
103
|
+
if ((pkg['devDependencies'] && pkg['devDependencies']['moleculer']) || (pkg['dependencies'] && pkg['dependencies']['moleculer'])) {
|
|
104
|
+
cicd[pkgName] = {
|
|
105
|
+
"output": "npm",
|
|
106
|
+
"language": "node",
|
|
107
|
+
"packageType": "moleculer",
|
|
108
|
+
"tier": "api"
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
} else if ((pkg['devDependencies'] && pkg['devDependencies']['react']) || (pkg['dependencies'] && pkg['dependencies']['react'])) {
|
|
112
|
+
cicd[pkgName] = {
|
|
113
|
+
"output": "npm",
|
|
114
|
+
"language": "JavaScript",
|
|
115
|
+
"packageType": "ux",
|
|
116
|
+
"tier": "ux"
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
} else {
|
|
120
|
+
// ok it is not node
|
|
121
|
+
var folderName = path.dirname(pkgPath);
|
|
122
|
+
const csproj = globbySync('*.csproj', { cwd: folderName, absolute: true });
|
|
123
|
+
const toml = globbySync('pyproject.toml', { cwd: folderName, absolute: true });
|
|
124
|
+
const docker = globbySync('Dockerfile', { cwd: folderName, absolute: true });
|
|
125
|
+
|
|
126
|
+
if (csproj.length > 0) {
|
|
127
|
+
cicd[pkgName] = {
|
|
128
|
+
"output": "nuget",
|
|
129
|
+
"language": "C#",
|
|
130
|
+
"packageType": "dotnet",
|
|
131
|
+
"tier": "api"
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
} else if (toml.length > 0) {
|
|
135
|
+
cicd[pkgName] = {
|
|
136
|
+
"output": "pypi",
|
|
137
|
+
"language": "Python",
|
|
138
|
+
"tier": "api"
|
|
139
|
+
}
|
|
140
|
+
if (pkgName.toLocaleLowerCase().endsWith("api")) cicd[pkgName]["packageType"] = "pypi";
|
|
141
|
+
return;
|
|
142
|
+
} else if (docker.length > 0 && (pkgPath.indexOf("image") >= 0 || pkgPath.indexOf("container") >= 0)) {
|
|
143
|
+
cicd[pkgName] = {
|
|
144
|
+
"output": "docker",
|
|
145
|
+
"language": "Dockerfile",
|
|
146
|
+
"tier": "infra"
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Ensure folderName and packageFolder are set on every cicd entry
|
|
156
|
+
Object.entries(nameToPath).forEach(([pkgName, pkgPath]) => {
|
|
157
|
+
if (cicd[pkgName]) {
|
|
158
|
+
if (!cicd[pkgName].folderName)
|
|
159
|
+
cicd[pkgName].folderName = path.basename(path.dirname(pkgPath));
|
|
160
|
+
if (!cicd[pkgName].packageFolder)
|
|
161
|
+
cicd[pkgName].packageFolder = path.basename(path.dirname(path.dirname(pkgPath)));
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Build pipeline file list, placeholder map, and folder→entry map from templated.json.
|
|
166
|
+
// pipelineFiles: ["build.yml", ...]
|
|
167
|
+
// placeholderMap: { pipelineFile: { PLACEHOLDER: { template, folderName } } }
|
|
168
|
+
// folderToEntry: { folderName: { pipelineFile, placeholder, template } }
|
|
169
|
+
let pipelineFiles = [];
|
|
170
|
+
const placeholderMap = {};
|
|
171
|
+
const folderToEntry = {};
|
|
172
|
+
let templatedConfig = {}; // folderName → [templates], used by pipeline generation
|
|
173
|
+
|
|
174
|
+
const templatedJsonPath = `./${configDir}/templates/templated.json`;
|
|
175
|
+
if (fs.existsSync(templatedJsonPath)) {
|
|
176
|
+
let data = JSON.parse(fs.readFileSync(templatedJsonPath, 'utf-8'));
|
|
177
|
+
let innerNames = {};
|
|
178
|
+
for (const pipelineFile of Object.keys(data)) {
|
|
179
|
+
pipelineFiles.push(pipelineFile);
|
|
180
|
+
placeholderMap[pipelineFile] = {};
|
|
181
|
+
for (const [placeholder, entry] of Object.entries(data[pipelineFile])) {
|
|
182
|
+
let rootPath = entry.rootPath.replace(/\/+$/, '').replace(/\.\.\/+/g, '');
|
|
183
|
+
const tpl = entry.template;
|
|
184
|
+
placeholderMap[pipelineFile][placeholder] = { template: tpl, folderName: rootPath };
|
|
185
|
+
innerNames[placeholder] = rootPath;
|
|
186
|
+
templatedConfig[rootPath] = templatedConfig[rootPath] || [];
|
|
187
|
+
if (!templatedConfig[rootPath].includes(tpl)) templatedConfig[rootPath].push(tpl);
|
|
188
|
+
if (!folderToEntry[rootPath]) {
|
|
189
|
+
folderToEntry[rootPath] = { pipelineFile, placeholder, template: tpl };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (const outerKey in data) {
|
|
195
|
+
if(!pipelines[outerKey]) {
|
|
196
|
+
let tpl = fs.readFileSync(`./${configDir}/templates/${outerKey}`, 'utf-8');
|
|
197
|
+
|
|
198
|
+
let i = 0;
|
|
199
|
+
for (const innerKey of Object.keys(innerNames)) {
|
|
200
|
+
const regex = new RegExp(`\{\s*${innerKey.toUpperCase()}\s*\}`, 'g');
|
|
201
|
+
if(i==0) tpl = tpl.replace(regex, ` blocks:\n - ${innerKey.toUpperCase()}`);
|
|
202
|
+
else tpl = tpl.replace(regex, ` - ${innerKey.toUpperCase()}`);
|
|
203
|
+
i++;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let yaml;
|
|
207
|
+
try {
|
|
208
|
+
yaml = YAML.parse(tpl);
|
|
209
|
+
} catch (e) {
|
|
210
|
+
if (options.debug) console.warn(`Skipping unparseable YAML template ${outerKey}: ${e.message}`);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let pipeline = pipelines[outerKey] = [];
|
|
215
|
+
for(const s of Object.keys(yaml.stages || {}))
|
|
216
|
+
{
|
|
217
|
+
let stage = yaml.stages[s];
|
|
218
|
+
|
|
219
|
+
let blocks = stage.blocks;
|
|
220
|
+
if(blocks) {
|
|
221
|
+
for(const b of blocks) {
|
|
222
|
+
let pgkList= stage2Packages[innerNames[b]];
|
|
223
|
+
if(pgkList) stage.packages = pgkList;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
delete stage.blocks;
|
|
228
|
+
delete stage.jobs;
|
|
229
|
+
delete stage.variables;
|
|
230
|
+
pipeline.push(stage);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
// Fallback: pipeline files = non-underscore .yml files in templates dir
|
|
236
|
+
const allTemplates = globbySync(`./${configDir}/templates/*.yml`);
|
|
237
|
+
pipelineFiles = allTemplates
|
|
238
|
+
.filter(f => !path.basename(f).includes('_'))
|
|
239
|
+
.map(f => path.basename(f));
|
|
240
|
+
// Placeholder keys = top-level children folder names uppercased
|
|
241
|
+
try {
|
|
242
|
+
const childrenFolders = fs.readdirSync('.').filter(f => {
|
|
243
|
+
try { return fs.statSync(f).isDirectory() && !f.startsWith('.') && f !== 'node_modules'; }
|
|
244
|
+
catch (e) { return false; }
|
|
245
|
+
});
|
|
246
|
+
for (const pipelineFile of pipelineFiles) {
|
|
247
|
+
placeholderMap[pipelineFile] = {};
|
|
248
|
+
for (const folder of childrenFolders) {
|
|
249
|
+
placeholderMap[pipelineFile][folder.toUpperCase()] = { template: null, folderName: folder };
|
|
250
|
+
if (!folderToEntry[folder]) {
|
|
251
|
+
folderToEntry[folder] = { pipelineFile, placeholder: folder.toUpperCase(), template: null };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} catch(e) {}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const templates = globbySync(`./${configDir}/templates/*.yml`);
|
|
259
|
+
const withUnderscore = templates
|
|
260
|
+
.filter(file => file.includes('_'))
|
|
261
|
+
.map(file => path.basename(file));
|
|
262
|
+
|
|
263
|
+
const codeScan = withUnderscore.filter(file => file.includes('scan') || file.includes('check'));
|
|
264
|
+
const build = withUnderscore.filter(file => !file.includes('scan') && !file.includes('check'));
|
|
265
|
+
|
|
266
|
+
// Load existing manifest to preserve any already-set templated values
|
|
267
|
+
const manifestPath = `./${configDir}/manifest.json`;
|
|
268
|
+
const existingManifest = fs.existsSync(manifestPath)
|
|
269
|
+
? JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
|
|
270
|
+
: {};
|
|
271
|
+
const existingTemplated = existingManifest.templated || {};
|
|
272
|
+
const existingCicd = existingManifest.cicd || {};
|
|
273
|
+
|
|
274
|
+
// Helper: get package name from project.json → version.json → pyproject.toml
|
|
275
|
+
const getPackageNameFromFiles = (pkgDir) => {
|
|
276
|
+
const projectJsonPath = path.join(pkgDir, 'project.json');
|
|
277
|
+
if (fs.existsSync(projectJsonPath)) {
|
|
278
|
+
try { const d = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8')); if (d.name) return d.name; } catch(e) {}
|
|
279
|
+
}
|
|
280
|
+
const versionJsonPath = path.join(pkgDir, 'version.json');
|
|
281
|
+
if (fs.existsSync(versionJsonPath)) {
|
|
282
|
+
try { const d = JSON.parse(fs.readFileSync(versionJsonPath, 'utf-8')); if (d.name) return d.name; } catch(e) {}
|
|
283
|
+
}
|
|
284
|
+
const pyprojectPath = path.join(pkgDir, 'pyproject.toml');
|
|
285
|
+
if (fs.existsSync(pyprojectPath)) {
|
|
286
|
+
try {
|
|
287
|
+
const content = fs.readFileSync(pyprojectPath, 'utf-8');
|
|
288
|
+
const match = content.match(/^\s*name\s*=\s*["']([^"']+)["']/m);
|
|
289
|
+
if (match) return match[1];
|
|
290
|
+
} catch(e) {}
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// Helper: derive best-fit template from cicd output type using underscore-prefixed template files
|
|
296
|
+
const deriveTemplate = (pkgName) => {
|
|
297
|
+
let lookfor = "node";
|
|
298
|
+
if (cicd[pkgName]["output"] == "pypi") lookfor = "python";
|
|
299
|
+
if (cicd[pkgName]["output"] == "nuget") lookfor = "dotnet";
|
|
300
|
+
if (cicd[pkgName]["output"] == "docker") lookfor = "docker";
|
|
301
|
+
for (const file of build) {
|
|
302
|
+
if (file.includes(lookfor)) return file;
|
|
303
|
+
}
|
|
304
|
+
return build[0] || null;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// Helper: find existing template assignment for a package name in existing manifest
|
|
308
|
+
const getExistingTemplate = (pkgName) => {
|
|
309
|
+
for (const pf of Object.keys(existingTemplated)) {
|
|
310
|
+
for (const ph of Object.keys(existingTemplated[pf] || {})) {
|
|
311
|
+
const val = (existingTemplated[pf][ph] || {})[pkgName];
|
|
312
|
+
if (val !== undefined) return { pipelineFile: pf, placeholder: ph, tpl: val };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// Start from existing templated config so updates are additive-only.
|
|
319
|
+
const templated = JSON.parse(JSON.stringify(existingTemplated));
|
|
320
|
+
|
|
321
|
+
// Ensure discovered pipeline file and placeholder keys exist without removing prior keys.
|
|
322
|
+
if (pipelineFiles.length === 0) pipelineFiles = ["build.yml"];
|
|
323
|
+
for (const pipelineFile of pipelineFiles) {
|
|
324
|
+
if (!templated[pipelineFile]) templated[pipelineFile] = {};
|
|
325
|
+
for (const placeholder of Object.keys(placeholderMap[pipelineFile] || {})) {
|
|
326
|
+
if (!templated[pipelineFile][placeholder]) templated[pipelineFile][placeholder] = {};
|
|
327
|
+
}
|
|
328
|
+
if (Object.keys(templated[pipelineFile]).length === 0) {
|
|
329
|
+
templated[pipelineFile]["PACKAGES"] = {};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
Object.entries(nameToPath).forEach(([pkgName, pkgPath]) => {
|
|
334
|
+
if (!cicd[pkgName]) return;
|
|
335
|
+
|
|
336
|
+
const pkgDir = path.dirname(pkgPath);
|
|
337
|
+
const folderName = path.basename(path.dirname(path.dirname(pkgPath)));
|
|
338
|
+
|
|
339
|
+
// Prefer package name from project.json/version.json/pyproject.toml
|
|
340
|
+
const preferredName = getPackageNameFromFiles(pkgDir) || pkgName;
|
|
341
|
+
|
|
342
|
+
// Find the pipeline file and placeholder for this package's folder
|
|
343
|
+
const entry = folderToEntry[folderName];
|
|
344
|
+
const pipelineFile = (entry && entry.pipelineFile) || pipelineFiles[0] || "build.yml";
|
|
345
|
+
let placeholder, template;
|
|
346
|
+
|
|
347
|
+
if (entry) {
|
|
348
|
+
placeholder = entry.placeholder;
|
|
349
|
+
template = entry.template;
|
|
350
|
+
} else {
|
|
351
|
+
// No folder mapping: pick best placeholder by output type
|
|
352
|
+
const lookfor = cicd[pkgName]["output"] == "pypi" ? "python" :
|
|
353
|
+
cicd[pkgName]["output"] == "nuget" ? "dotnet" :
|
|
354
|
+
cicd[pkgName]["output"] == "docker" ? "docker" : "node";
|
|
355
|
+
let bestPh = null, bestTpl = deriveTemplate(pkgName);
|
|
356
|
+
for (const [ph, info] of Object.entries(placeholderMap[pipelineFile] || {})) {
|
|
357
|
+
if (info.template && info.template.toLowerCase().includes(lookfor)) {
|
|
358
|
+
bestPh = ph;
|
|
359
|
+
bestTpl = info.template;
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
placeholder = bestPh || "PACKAGES";
|
|
364
|
+
template = bestTpl;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!templated[pipelineFile]) templated[pipelineFile] = {};
|
|
368
|
+
if (!templated[pipelineFile][placeholder]) templated[pipelineFile][placeholder] = {};
|
|
369
|
+
|
|
370
|
+
// Additive-only behavior: keep any existing assignment and only add missing entries.
|
|
371
|
+
const existing = getExistingTemplate(preferredName) || getExistingTemplate(pkgName);
|
|
372
|
+
if (!existing) {
|
|
373
|
+
templated[pipelineFile][placeholder][preferredName] = template || deriveTemplate(pkgName);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Merge in any pre-existing cicd fields (hash, folderName, packageFolder set by release:finalize)
|
|
378
|
+
Object.keys(existingCicd).forEach(pkgName => {
|
|
379
|
+
if (cicd[pkgName]) {
|
|
380
|
+
// Preserve fields from the existing entry that we don't regenerate
|
|
381
|
+
if (existingCicd[pkgName].hash) cicd[pkgName].hash = existingCicd[pkgName].hash;
|
|
382
|
+
if (existingCicd[pkgName].version) cicd[pkgName].version = existingCicd[pkgName].version;
|
|
383
|
+
} else {
|
|
384
|
+
// If package was in cicd before but not now, preserve entire entry
|
|
385
|
+
cicd[pkgName] = existingCicd[pkgName];
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Final output structure
|
|
390
|
+
const output = {
|
|
391
|
+
versions,
|
|
392
|
+
dependencies,
|
|
393
|
+
dependents,
|
|
394
|
+
pipelines,
|
|
395
|
+
templated,
|
|
396
|
+
cicd
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const outputJson = JSON.stringify(output, null, 2)
|
|
400
|
+
if(options.save) fs.writeFileSync(`./${configDir}/manifest.json`, outputJson, 'utf-8');
|
|
401
|
+
return outputJson;
|
|
402
|
+
};
|
package/src/options.mjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const getDefaultOptions = () => ({
|
|
2
|
+
debug: false,
|
|
3
|
+
platform: null,
|
|
4
|
+
hashExcludeFolders: ['node_modules', 'coverage', 'dist'],
|
|
5
|
+
hashExcludeFiles: ['.npmrc', 'CHANGELOG.md', 'README.md']
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const parseCliArgs = (argv = process.argv) => {
|
|
9
|
+
const options = getDefaultOptions();
|
|
10
|
+
for (let i = 2; i < argv.length; i++) {
|
|
11
|
+
if (argv[i] === '--save') options.save = true;
|
|
12
|
+
else if (argv[i] === '--debug') options.debug = true;
|
|
13
|
+
else if (argv[i] === '--changed') options.changed = true;
|
|
14
|
+
else if (argv[i] === '--hash') options.hash = true;
|
|
15
|
+
else if (argv[i] === '--platform') {
|
|
16
|
+
options.platform = argv[i + 1];
|
|
17
|
+
i++;
|
|
18
|
+
} else if (argv[i] === '--hashExcludeFolders' || argv[i] === '--hashExcludeFiles') {
|
|
19
|
+
const name = argv[i].substring(2);
|
|
20
|
+
options[name] = argv[i + 1].split(',');
|
|
21
|
+
i++;
|
|
22
|
+
} else if (argv[i].substring(0, 2) === '--') {
|
|
23
|
+
const name = argv[i].substring(2);
|
|
24
|
+
if (options[name] !== undefined) {
|
|
25
|
+
options[name] = argv[i + 1];
|
|
26
|
+
i++;
|
|
27
|
+
} else {
|
|
28
|
+
throw new Error(`Expected a known option, got ${argv[i]}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return options;
|
|
33
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { describe, it, expect } from '@jest/globals';
|
|
5
|
+
import { generateManifest } from '../src/manifest.mjs';
|
|
6
|
+
|
|
7
|
+
const writeJson = (filePath, obj) => {
|
|
8
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
9
|
+
fs.writeFileSync(filePath, JSON.stringify(obj, null, 2), 'utf-8');
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe('generateManifest templated behavior', () => {
|
|
13
|
+
it('keeps existing templated mappings and adds missing packages only', async () => {
|
|
14
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'manifest-tests-'));
|
|
15
|
+
const oldCwd = process.cwd();
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
writeJson(path.join(tmpRoot, 'packages/lib-a/package.json'), {
|
|
19
|
+
name: 'lib-a',
|
|
20
|
+
version: '1.0.0',
|
|
21
|
+
main: 'index.js'
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
writeJson(path.join(tmpRoot, 'packages/lib-b/package.json'), {
|
|
25
|
+
name: 'lib-b',
|
|
26
|
+
version: '1.0.0',
|
|
27
|
+
main: 'index.js'
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
fs.mkdirSync(path.join(tmpRoot, '.cicd/templates'), { recursive: true });
|
|
31
|
+
fs.writeFileSync(path.join(tmpRoot, '.cicd/templates/build.yml'), 'stages: []\n', 'utf-8');
|
|
32
|
+
fs.writeFileSync(path.join(tmpRoot, '.cicd/templates/_build_node_package.yml'), '# node build template\n', 'utf-8');
|
|
33
|
+
|
|
34
|
+
writeJson(path.join(tmpRoot, '.cicd/manifest.json'), {
|
|
35
|
+
templated: {
|
|
36
|
+
'build.yml': {
|
|
37
|
+
PACKAGES: {
|
|
38
|
+
'lib-a': 'custom_existing_template.yml'
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
'legacy.yml': {
|
|
42
|
+
PACKAGES: {
|
|
43
|
+
'legacy-pkg': 'legacy_template.yml'
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
process.chdir(tmpRoot);
|
|
50
|
+
const output = JSON.parse(await generateManifest({ platform: 'ado' }));
|
|
51
|
+
|
|
52
|
+
expect(output.templated['build.yml'].PACKAGES['lib-a']).toBe('custom_existing_template.yml');
|
|
53
|
+
expect(output.templated['build.yml'].PACKAGES['lib-b']).toBe('_build_node_package.yml');
|
|
54
|
+
|
|
55
|
+
expect(output.templated['legacy.yml']).toBeDefined();
|
|
56
|
+
expect(output.templated['legacy.yml'].PACKAGES['legacy-pkg']).toBe('legacy_template.yml');
|
|
57
|
+
} finally {
|
|
58
|
+
process.chdir(oldCwd);
|
|
59
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('ignores unparseable YAML templates and continues', async () => {
|
|
64
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'manifest-tests-'));
|
|
65
|
+
const oldCwd = process.cwd();
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
writeJson(path.join(tmpRoot, 'packages/lib-a/package.json'), {
|
|
69
|
+
name: 'lib-a',
|
|
70
|
+
version: '1.0.0',
|
|
71
|
+
main: 'index.js'
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
fs.mkdirSync(path.join(tmpRoot, '.cicd/templates'), { recursive: true });
|
|
75
|
+
fs.writeFileSync(path.join(tmpRoot, '.cicd/templates/good.yml'), 'stages:\n build:\n name: Build\n', 'utf-8');
|
|
76
|
+
fs.writeFileSync(path.join(tmpRoot, '.cicd/templates/bad.yml'), 'stages:\n build: [\n', 'utf-8');
|
|
77
|
+
fs.writeFileSync(path.join(tmpRoot, '.cicd/templates/_build_node_package.yml'), '# node build template\n', 'utf-8');
|
|
78
|
+
|
|
79
|
+
writeJson(path.join(tmpRoot, '.cicd/templates/templated.json'), {
|
|
80
|
+
'good.yml': {
|
|
81
|
+
PACKAGES: {
|
|
82
|
+
rootPath: '../packages',
|
|
83
|
+
template: 'build_node_package.yml'
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
'bad.yml': {
|
|
87
|
+
PACKAGES: {
|
|
88
|
+
rootPath: '../packages',
|
|
89
|
+
template: 'build_node_package.yml'
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
process.chdir(tmpRoot);
|
|
95
|
+
const output = JSON.parse(await generateManifest({ platform: 'ado' }));
|
|
96
|
+
|
|
97
|
+
expect(output.pipelines['good.yml']).toBeDefined();
|
|
98
|
+
expect(Array.isArray(output.pipelines['good.yml'])).toBe(true);
|
|
99
|
+
expect(output.pipelines['bad.yml']).toBeUndefined();
|
|
100
|
+
} finally {
|
|
101
|
+
process.chdir(oldCwd);
|
|
102
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import { getDefaultOptions, parseCliArgs } from '../src/options.mjs';
|
|
3
|
+
|
|
4
|
+
describe('manifest CLI options', () => {
|
|
5
|
+
it('returns expected defaults', () => {
|
|
6
|
+
expect(getDefaultOptions()).toEqual({
|
|
7
|
+
debug: false,
|
|
8
|
+
platform: null,
|
|
9
|
+
hashExcludeFolders: ['node_modules', 'coverage', 'dist'],
|
|
10
|
+
hashExcludeFiles: ['.npmrc', 'CHANGELOG.md', 'README.md']
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('parses boolean and valued flags', () => {
|
|
15
|
+
const options = parseCliArgs([
|
|
16
|
+
'node',
|
|
17
|
+
'index.mjs',
|
|
18
|
+
'--save',
|
|
19
|
+
'--debug',
|
|
20
|
+
'--platform',
|
|
21
|
+
'github',
|
|
22
|
+
'--hashExcludeFolders',
|
|
23
|
+
'node_modules,dist',
|
|
24
|
+
'--hashExcludeFiles',
|
|
25
|
+
'README.md,CHANGELOG.md'
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
expect(options.save).toBe(true);
|
|
29
|
+
expect(options.debug).toBe(true);
|
|
30
|
+
expect(options.platform).toBe('github');
|
|
31
|
+
expect(options.hashExcludeFolders).toEqual(['node_modules', 'dist']);
|
|
32
|
+
expect(options.hashExcludeFiles).toEqual(['README.md', 'CHANGELOG.md']);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('throws on unknown option', () => {
|
|
36
|
+
expect(() => parseCliArgs(['node', 'index.mjs', '--nope'])).toThrow(
|
|
37
|
+
'Expected a known option, got --nope'
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
});
|