enablement-build-template-generate 1.0.19 → 1.0.20
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/README.md +3 -0
- package/package.json +7 -7
- package/src/index.mjs +36 -4
- package/tests/index.test.mjs +185 -0
package/README.md
CHANGED
|
@@ -101,6 +101,9 @@ Each job snippet template is expanded once per package. The following placeholde
|
|
|
101
101
|
| `{full}` | Full npm package name including scope (e.g. `@scope/my-component`) |
|
|
102
102
|
| `{path}` | The package folder root (from `cicd[pkg].packageFolder`, e.g. `packages`) |
|
|
103
103
|
| `{other}` | Expands to a comma-separated list of paths to every **other** package in the same variable — useful for pipeline dependency declarations |
|
|
104
|
+
| `{metadataKey}` | Custom placeholders like `{owner}` or `{tier}` are replaced from `manifest.cicd[packageName].metadata` (**manifest.json mode only**) |
|
|
105
|
+
|
|
106
|
+
In `templated.json` legacy mode, custom metadata placeholders are not supported and remain unchanged in output.
|
|
104
107
|
|
|
105
108
|
---
|
|
106
109
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "enablement-build-template-generate",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.20",
|
|
4
4
|
"description": "Generates build yaml for mono repos",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./src/index.mjs",
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
"devDependencies": {
|
|
14
14
|
"jest": "^30.4.2"
|
|
15
15
|
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node src/index.mjs",
|
|
18
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
19
|
+
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch"
|
|
20
|
+
},
|
|
16
21
|
"jest": {
|
|
17
22
|
"testEnvironment": "node",
|
|
18
23
|
"testMatch": [
|
|
@@ -20,10 +25,5 @@
|
|
|
20
25
|
]
|
|
21
26
|
},
|
|
22
27
|
"license": "MIT",
|
|
23
|
-
"author": "Contributors"
|
|
24
|
-
"scripts": {
|
|
25
|
-
"start": "node src/index.mjs",
|
|
26
|
-
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
27
|
-
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch"
|
|
28
|
-
}
|
|
28
|
+
"author": "Contributors"
|
|
29
29
|
}
|
package/src/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as url from 'url';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs';
|
|
5
5
|
import { globby } from 'globby';
|
|
6
6
|
|
|
7
7
|
import {
|
|
@@ -32,6 +32,38 @@ function readJobTemplate(templateFile, templatePath, fallbackTemplatePath) {
|
|
|
32
32
|
throw new Error(`Template file not found: ${templateFile} (looked in ${templatePath}${fallbackTemplatePath ? ' and ' + fallbackTemplatePath : ''})`);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
|
|
36
|
+
// Resolve package folder name for {name}: prefer cicd.folderName, otherwise infer from disk.
|
|
37
|
+
function resolveFolderName(pkgName, info = {}) {
|
|
38
|
+
if (info.folderName) return info.folderName;
|
|
39
|
+
|
|
40
|
+
const packageFolder = info.packageFolder || 'packages';
|
|
41
|
+
const packageRoot = path.join(process.cwd(), packageFolder);
|
|
42
|
+
if (!existsSync(packageRoot)) return resolvePackageName(pkgName);
|
|
43
|
+
|
|
44
|
+
const dirs = readdirSync(packageRoot, { withFileTypes: true })
|
|
45
|
+
.filter(entry => entry.isDirectory())
|
|
46
|
+
.map(entry => entry.name);
|
|
47
|
+
|
|
48
|
+
for (const dirName of dirs) {
|
|
49
|
+
const base = path.join(packageRoot, dirName);
|
|
50
|
+
const candidates = ['package.json', 'version.json', 'pyproject.toml'];
|
|
51
|
+
|
|
52
|
+
for (const candidate of candidates) {
|
|
53
|
+
const manifestPath = path.join(base, candidate);
|
|
54
|
+
if (!existsSync(manifestPath)) continue;
|
|
55
|
+
|
|
56
|
+
const ext = path.extname(candidate).substring(1).toLowerCase();
|
|
57
|
+
const content = readFileSync(manifestPath, 'utf8');
|
|
58
|
+
const parsedName = parseManifestName(content, ext);
|
|
59
|
+
|
|
60
|
+
if (parsedName === pkgName) return dirName;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return resolvePackageName(pkgName);
|
|
65
|
+
}
|
|
66
|
+
|
|
35
67
|
// ---------------------------------------------------------------------------
|
|
36
68
|
// Expand using manifest.json "templated" section (new format)
|
|
37
69
|
// ---------------------------------------------------------------------------
|
|
@@ -40,11 +72,11 @@ function expandVarFromManifest(varname, packageMap, cicd, templatePath, fallback
|
|
|
40
72
|
try {
|
|
41
73
|
const packageNames = Object.keys(packageMap).filter(pkg => {
|
|
42
74
|
if (packageMap[pkg] === false) return false;
|
|
43
|
-
const folderName = (cicd[pkg] || {})
|
|
75
|
+
const folderName = resolveFolderName(pkg, cicd[pkg] || {});
|
|
44
76
|
return !options.exclude.includes(folderName);
|
|
45
77
|
});
|
|
46
78
|
|
|
47
|
-
const allFolderNames = packageNames.map(pkg => (cicd[pkg] || {})
|
|
79
|
+
const allFolderNames = packageNames.map(pkg => resolveFolderName(pkg, cicd[pkg] || {}));
|
|
48
80
|
|
|
49
81
|
const folderCounts = {};
|
|
50
82
|
packageNames.forEach(pkg => {
|
|
@@ -60,7 +92,7 @@ function expandVarFromManifest(varname, packageMap, cicd, templatePath, fallback
|
|
|
60
92
|
: [packageMap[pkgName]];
|
|
61
93
|
|
|
62
94
|
const info = cicd[pkgName] || {};
|
|
63
|
-
const name = info
|
|
95
|
+
const name = resolveFolderName(pkgName, info);
|
|
64
96
|
const jobRoot = info.packageFolder || 'packages';
|
|
65
97
|
const safe = makeSafe(name);
|
|
66
98
|
const packageName = resolvePackageName(pkgName);
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { execFileSync } from 'child_process';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
function writeJson(filePath, data) {
|
|
9
|
+
writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function createFixtureWorkspace(mode, options = {}) {
|
|
13
|
+
const tmpRoot = mkdtempSync(path.join(os.tmpdir(), 'template-generate-'));
|
|
14
|
+
|
|
15
|
+
const defaultJobTemplateLines = [
|
|
16
|
+
'- job: build_{safe}',
|
|
17
|
+
' displayName: NAME={name}',
|
|
18
|
+
' safe: SAFE={safe}',
|
|
19
|
+
' package: PKG={package}',
|
|
20
|
+
' full: FULL={full}',
|
|
21
|
+
' other: OTHER={other}',
|
|
22
|
+
" script: cd ./{path}/{name}",
|
|
23
|
+
" condition: contains(variables['changed'], '{name}')",
|
|
24
|
+
'',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const jobTemplateLines = options.jobTemplateLines || defaultJobTemplateLines;
|
|
28
|
+
|
|
29
|
+
mkdirSync(path.join(tmpRoot, '.cicd', 'templates'), { recursive: true });
|
|
30
|
+
mkdirSync(path.join(tmpRoot, 'packages', 'alpha-lib'), { recursive: true });
|
|
31
|
+
mkdirSync(path.join(tmpRoot, 'packages', 'beta-tool'), { recursive: true });
|
|
32
|
+
|
|
33
|
+
writeJson(path.join(tmpRoot, 'packages', 'alpha-lib', 'package.json'), {
|
|
34
|
+
name: '@scope/enablement-build-alpha-lib',
|
|
35
|
+
version: '1.0.0',
|
|
36
|
+
});
|
|
37
|
+
writeJson(path.join(tmpRoot, 'packages', 'beta-tool', 'package.json'), {
|
|
38
|
+
name: 'enablement-build-beta-tool',
|
|
39
|
+
version: '1.0.0',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
writeFileSync(
|
|
43
|
+
path.join(tmpRoot, '.cicd', 'templates', 'build.yml'),
|
|
44
|
+
'jobs:\n{PACKAGES}\n',
|
|
45
|
+
'utf8'
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
writeFileSync(
|
|
49
|
+
path.join(tmpRoot, '.cicd', 'templates', 'job.yml'),
|
|
50
|
+
jobTemplateLines.join('\n'),
|
|
51
|
+
'utf8'
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const cicdEntries = {
|
|
55
|
+
'@scope/enablement-build-alpha-lib': {
|
|
56
|
+
packageFolder: 'packages',
|
|
57
|
+
...(options.metadataByPackage?.['@scope/enablement-build-alpha-lib'] || {}),
|
|
58
|
+
},
|
|
59
|
+
'enablement-build-beta-tool': {
|
|
60
|
+
packageFolder: 'packages',
|
|
61
|
+
...(options.metadataByPackage?.['enablement-build-beta-tool'] || {}),
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (mode === 'manifest') {
|
|
66
|
+
writeJson(path.join(tmpRoot, '.cicd', 'manifest.json'), {
|
|
67
|
+
templated: {
|
|
68
|
+
'build.yml': {
|
|
69
|
+
PACKAGES: {
|
|
70
|
+
'@scope/enablement-build-alpha-lib': 'job.yml',
|
|
71
|
+
'enablement-build-beta-tool': 'job.yml',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
cicd: cicdEntries,
|
|
76
|
+
});
|
|
77
|
+
} else {
|
|
78
|
+
writeJson(path.join(tmpRoot, '.cicd', 'templates', 'templated.json'), {
|
|
79
|
+
'build.yml': {
|
|
80
|
+
PACKAGES: {
|
|
81
|
+
rootPath: '../../packages',
|
|
82
|
+
template: 'job.yml',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return tmpRoot;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function runGenerator(cwd) {
|
|
92
|
+
const scriptPath = fileURLToPath(new URL('../src/index.mjs', import.meta.url));
|
|
93
|
+
execFileSync(process.execPath, [scriptPath, '--platform', 'ado'], {
|
|
94
|
+
cwd,
|
|
95
|
+
stdio: 'pipe',
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return readFileSync(path.join(cwd, '.cicd', 'build.yml'), 'utf8');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function stripBanner(content) {
|
|
102
|
+
return content
|
|
103
|
+
.split('\n')
|
|
104
|
+
.filter(
|
|
105
|
+
(line) =>
|
|
106
|
+
line !== '#######################################' &&
|
|
107
|
+
line !== '### DO NOT EDIT - THIS IS GENERATED ###'
|
|
108
|
+
)
|
|
109
|
+
.join('\n')
|
|
110
|
+
.trim();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function normalizeOutput(content, root) {
|
|
114
|
+
return stripBanner(content).split(root).join('<ROOT>');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
describe('template-generate mode parity', () => {
|
|
118
|
+
it('expands placeholders consistently between manifest and templated.json modes', () => {
|
|
119
|
+
const manifestRoot = createFixtureWorkspace('manifest');
|
|
120
|
+
const legacyRoot = createFixtureWorkspace('legacy');
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const manifestOut = runGenerator(manifestRoot);
|
|
124
|
+
const legacyOut = runGenerator(legacyRoot);
|
|
125
|
+
|
|
126
|
+
expect(manifestOut).toContain('NAME=alpha-lib');
|
|
127
|
+
expect(manifestOut).toContain('NAME=beta-tool');
|
|
128
|
+
expect(manifestOut).toContain('SAFE=alphalib');
|
|
129
|
+
expect(manifestOut).toContain('SAFE=betatool');
|
|
130
|
+
expect(manifestOut).toContain('PKG=enablement-build-alpha-lib');
|
|
131
|
+
expect(manifestOut).toContain('PKG=enablement-build-beta-tool');
|
|
132
|
+
expect(manifestOut).toContain('FULL=@scope/enablement-build-alpha-lib');
|
|
133
|
+
expect(manifestOut).toContain('FULL=enablement-build-beta-tool');
|
|
134
|
+
expect(manifestOut).toContain(`OTHER=${manifestRoot}/packages/beta-tool`);
|
|
135
|
+
expect(manifestOut).toContain(`OTHER=${manifestRoot}/packages/alpha-lib`);
|
|
136
|
+
expect(manifestOut).toContain('script: cd ./packages/alpha-lib');
|
|
137
|
+
expect(manifestOut).toContain('script: cd ./packages/beta-tool');
|
|
138
|
+
expect(manifestOut).toContain("condition: contains(variables['changed'], 'alpha-lib')");
|
|
139
|
+
expect(manifestOut).toContain("condition: contains(variables['changed'], 'beta-tool')");
|
|
140
|
+
|
|
141
|
+
expect(normalizeOutput(manifestOut, manifestRoot)).toBe(normalizeOutput(legacyOut, legacyRoot));
|
|
142
|
+
} finally {
|
|
143
|
+
rmSync(manifestRoot, { recursive: true, force: true });
|
|
144
|
+
rmSync(legacyRoot, { recursive: true, force: true });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('applies custom metadata placeholders only in manifest mode', () => {
|
|
149
|
+
const options = {
|
|
150
|
+
jobTemplateLines: [
|
|
151
|
+
'- job: meta_{safe}',
|
|
152
|
+
' owner: OWNER={owner}',
|
|
153
|
+
' tier: TIER={tier}',
|
|
154
|
+
' name: NAME={name}',
|
|
155
|
+
'',
|
|
156
|
+
],
|
|
157
|
+
metadataByPackage: {
|
|
158
|
+
'@scope/enablement-build-alpha-lib': {
|
|
159
|
+
metadata: { owner: 'team-alpha', tier: 'gold' },
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const manifestRoot = createFixtureWorkspace('manifest', options);
|
|
165
|
+
const legacyRoot = createFixtureWorkspace('legacy', options);
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const manifestOut = runGenerator(manifestRoot);
|
|
169
|
+
const legacyOut = runGenerator(legacyRoot);
|
|
170
|
+
|
|
171
|
+
expect(manifestOut).toContain('owner: OWNER=team-alpha');
|
|
172
|
+
expect(manifestOut).toContain('tier: TIER=gold');
|
|
173
|
+
expect(manifestOut).toContain('owner: OWNER={owner}');
|
|
174
|
+
expect(manifestOut).toContain('tier: TIER={tier}');
|
|
175
|
+
|
|
176
|
+
expect(legacyOut).not.toContain('owner: OWNER=team-alpha');
|
|
177
|
+
expect(legacyOut).not.toContain('tier: TIER=gold');
|
|
178
|
+
expect(legacyOut).toContain('owner: OWNER={owner}');
|
|
179
|
+
expect(legacyOut).toContain('tier: TIER={tier}');
|
|
180
|
+
} finally {
|
|
181
|
+
rmSync(manifestRoot, { recursive: true, force: true });
|
|
182
|
+
rmSync(legacyRoot, { recursive: true, force: true });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
});
|