enablement-build-template-generate 1.0.17 → 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 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,10 +1,11 @@
1
1
  {
2
2
  "name": "enablement-build-template-generate",
3
- "version": "1.0.17",
3
+ "version": "1.0.20",
4
4
  "description": "Generates build yaml for mono repos",
5
5
  "type": "module",
6
6
  "module": "./src/index.mjs",
7
7
  "main": "./src/index.mjs",
8
+ "bin": "./src/index.mjs",
8
9
  "dependencies": {
9
10
  "globby": "^16.2.0",
10
11
  "user": "^0.0.0"
@@ -12,6 +13,11 @@
12
13
  "devDependencies": {
13
14
  "jest": "^30.4.2"
14
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
+ },
15
21
  "jest": {
16
22
  "testEnvironment": "node",
17
23
  "testMatch": [
@@ -19,13 +25,5 @@
19
25
  ]
20
26
  },
21
27
  "license": "MIT",
22
- "author": "Contributors",
23
- "scripts": {
24
- "start": "node src/index.mjs",
25
- "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
26
- "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch"
27
- },
28
- "bin": {
29
- "enablement-build-template-generate": "./src/index.mjs"
30
- }
28
+ "author": "Contributors"
31
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] || {}).folderName || 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] || {}).folderName || 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.folderName || pkgName;
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);
@@ -94,8 +126,8 @@ function expandVarFromTemplatedJson(varname, jobConfig, templatePath, fallbackTe
94
126
  const allNames = Object.keys(folderBest).sort().filter(n => !options.exclude.includes(n));
95
127
 
96
128
  const expandedJobs = allNames.map(name => {
97
- const content = readFileSync(path.join(cwd, folderBest[name]), 'utf8');
98
- const ext = path.extname(folderBest[name]).substring(1).toLowerCase();
129
+ const content = readFileSync(path.join(cwd, folderBest[name].filePath), 'utf8');
130
+ const ext = path.extname(folderBest[name].filePath).substring(1).toLowerCase();
99
131
  const fullName = parseManifestName(content, ext) || name;
100
132
  const safe = makeSafe(name);
101
133
  const packageName = resolvePackageName(fullName);
@@ -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
+ });