enablement-build-template-generate 1.0.19 → 1.3.0
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 +30 -31
- package/package.json +3 -2
- package/src/index.mjs +216 -71
- package/src/lib.mjs +131 -0
- package/tests/index.test.mjs +545 -0
- package/tests/lib.test.mjs +208 -0
package/README.md
CHANGED
|
@@ -12,11 +12,13 @@ npx enablement-build-template-generate@latest [flags]
|
|
|
12
12
|
|
|
13
13
|
## How it works
|
|
14
14
|
|
|
15
|
-
1. **
|
|
16
|
-
2. **Read config** — If `manifest.json`
|
|
17
|
-
3. **
|
|
18
|
-
4. **
|
|
19
|
-
5. **
|
|
15
|
+
1. **Select platform** — Uses Azure DevOps (`.cicd/`) by default. Pass `--platform github` to use GitHub (`.github/`).
|
|
16
|
+
2. **Read config** — `{configDir}/templates/templated.json` is the source of truth. If `manifest.json` still has a `templated` section (from older tooling), it's read once, merged into `templated.json`, and removed from `manifest.json`. A `templated.json` in the legacy `{ rootPath, template }` shape is converted to the current format (packages discovered by globbing `rootPath`).
|
|
17
|
+
3. **Detect new packages** — Any package not yet assigned a template (scanned via `--children`/auto-detected workspace roots) is classified by type (node/python/dotnet/docker) and assigned a template, reusing the convention of other packages in the same folder or of the same type where possible.
|
|
18
|
+
4. **Persist** — `templated.json` (and `manifest.json`, if it needed migrating) is written back, only when something actually changed.
|
|
19
|
+
5. **Expand job templates** — For each package, reads its job snippet template and fills in placeholders (`folderName`/`packageFolder`/`metadata` come from `templated.json`'s own `cicd` section). Packages are ordered so that, per `manifest.json`'s `dependencies` graph (when present), a package's local dependencies are generated before it, as much as possible.
|
|
20
|
+
6. **Stitch** — Joins all expanded job snippets and substitutes them into the document template at the named placeholder.
|
|
21
|
+
7. **Write output** — Writes the final file one level above the template directory (`.github/templates/` → `.github/` or `.cicd/templates/` → `.cicd/`), wrapped in `DO NOT EDIT` banners.
|
|
20
22
|
|
|
21
23
|
---
|
|
22
24
|
|
|
@@ -24,22 +26,22 @@ npx enablement-build-template-generate@latest [flags]
|
|
|
24
26
|
|
|
25
27
|
| Flag | Default | Description |
|
|
26
28
|
|---|---|---|
|
|
27
|
-
| `--platform <name>` |
|
|
28
|
-
| `--
|
|
29
|
+
| `--platform <name>` | `ado` | Select the platform: `ado` (Azure DevOps) or `github` (GitHub Actions). |
|
|
30
|
+
| `--children <dirs>` | auto-detect | Comma- or space-separated list of top-level directories to scan when detecting new (not-yet-configured) packages. When omitted, directories are read from `pnpm-workspace.yaml` or `package.json` workspaces automatically. |
|
|
31
|
+
| `--exclude <list>` | — | Comma- or space-separated list of names to skip: package folder names during generation, and top-level directories (from `--children` or auto-detection) during new-package scanning. |
|
|
29
32
|
| `--debug` | off | Print verbose output. |
|
|
30
33
|
|
|
31
34
|
---
|
|
32
35
|
|
|
33
36
|
## Config source
|
|
34
37
|
|
|
35
|
-
|
|
38
|
+
### `templated.json` (current format)
|
|
36
39
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
Reads the `templated` section of `{configDir}/manifest.json` (produced by `enablement-build-manifest`). Each key is the **output filename**; each value maps a placeholder variable name to a per-package template mapping.
|
|
40
|
+
`{configDir}/templates/templated.json` is the source of truth for template routing. Each key under `templated` is the **output filename**; each value maps a placeholder variable name to a per-package template mapping. `cicd` carries the per-package metadata generation needs.
|
|
40
41
|
|
|
41
42
|
```json
|
|
42
43
|
{
|
|
44
|
+
"version": 2,
|
|
43
45
|
"templated": {
|
|
44
46
|
"build.yml": {
|
|
45
47
|
"PACKAGES": {
|
|
@@ -48,30 +50,24 @@ Reads the `templated` section of `{configDir}/manifest.json` (produced by `enabl
|
|
|
48
50
|
"my-scope/python-lib": ["build_python_package.yml", "build_scan.yml"]
|
|
49
51
|
}
|
|
50
52
|
}
|
|
53
|
+
},
|
|
54
|
+
"cicd": {
|
|
55
|
+
"my-scope/component-a": { "folderName": "component-a", "packageFolder": "packages" }
|
|
51
56
|
}
|
|
52
57
|
}
|
|
53
58
|
```
|
|
54
59
|
|
|
55
60
|
Each package name maps to a **single template filename** or an **array of template filenames** (all expanded in order for that package). Template lookup tries the active platform templates directory first (`.cicd/templates/` or `.github/templates/`) and then falls back to the other platform directory if a file is missing.
|
|
56
61
|
|
|
57
|
-
Package metadata (`folderName`, `packageFolder`) is read from `
|
|
62
|
+
Package metadata (`folderName`, `packageFolder`, `metadata`) is read from `templated.json`'s own `cicd` map and used to fill template placeholders.
|
|
58
63
|
|
|
59
|
-
|
|
64
|
+
A `templated.json` in the legacy `{ rootPath, template }` shape (no top-level `templated`/`version` key) is converted to the current format automatically — packages are discovered by globbing `rootPath` for `package.json`/`version.json`/`pyproject.toml`, and the result is written back.
|
|
60
65
|
|
|
61
|
-
|
|
66
|
+
### `manifest.json` (one-time migration source + dependency graph)
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
{
|
|
65
|
-
"build.yml": {
|
|
66
|
-
"PACKAGES": {
|
|
67
|
-
"rootPath": "../../packages",
|
|
68
|
-
"template": "build_node_component.yml"
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
```
|
|
68
|
+
If `{configDir}/manifest.json` still has a `templated` section (from older tooling, or `enablement-build-manifest` before it stopped writing one), it's merged into `templated.json` and removed from `manifest.json` — existing assignments in `templated.json` win over stale ones from `manifest.json`.
|
|
73
69
|
|
|
74
|
-
|
|
70
|
+
Independently of that migration, when `manifest.json` exists its `dependencies` map (`{ "<package>": ["<local-dep>", ...] }`) is used to order generation: within each placeholder, a package's local dependencies are generated before it, as much as possible (dependencies outside that placeholder's package list, or on a package that also depends back on it, are simply skipped rather than causing an error).
|
|
75
71
|
|
|
76
72
|
---
|
|
77
73
|
|
|
@@ -101,6 +97,9 @@ Each job snippet template is expanded once per package. The following placeholde
|
|
|
101
97
|
| `{full}` | Full npm package name including scope (e.g. `@scope/my-component`) |
|
|
102
98
|
| `{path}` | The package folder root (from `cicd[pkg].packageFolder`, e.g. `packages`) |
|
|
103
99
|
| `{other}` | Expands to a comma-separated list of paths to every **other** package in the same variable — useful for pipeline dependency declarations |
|
|
100
|
+
| `{metadataKey}` | Custom placeholders like `{owner}` or `{tier}` are replaced from `templated.json`'s `cicd[packageName].metadata` |
|
|
101
|
+
|
|
102
|
+
Custom metadata placeholders aren't supported for a package still being discovered via the legacy `{ rootPath, template }` conversion (they remain unchanged in output) — once converted, `templated.json`'s `cicd` map can carry `metadata` for any package.
|
|
104
103
|
|
|
105
104
|
---
|
|
106
105
|
|
|
@@ -108,25 +107,25 @@ Each job snippet template is expanded once per package. The following placeholde
|
|
|
108
107
|
|
|
109
108
|
Job template files live in the templates subfolder; document templates and generated output files are one level up.
|
|
110
109
|
|
|
111
|
-
**Azure DevOps
|
|
110
|
+
**Azure DevOps**:
|
|
112
111
|
```
|
|
113
112
|
.cicd/
|
|
114
|
-
├── manifest.json ←
|
|
113
|
+
├── manifest.json ← optional: dependency graph for ordering, one-time templated migration source
|
|
115
114
|
├── build.yml ← generated output
|
|
116
115
|
└── templates/
|
|
117
116
|
├── build.yml ← document template
|
|
118
|
-
├── templated.json ←
|
|
117
|
+
├── templated.json ← source of truth for templated config
|
|
119
118
|
└── build_node_component.yml ← job snippet template
|
|
120
119
|
```
|
|
121
120
|
|
|
122
|
-
**GitHub Actions
|
|
121
|
+
**GitHub Actions**:
|
|
123
122
|
```
|
|
124
123
|
.github/
|
|
125
|
-
├── manifest.json ←
|
|
124
|
+
├── manifest.json ← optional: dependency graph for ordering, one-time templated migration source
|
|
126
125
|
├── build.yml ← generated output
|
|
127
126
|
└── templates/
|
|
128
127
|
├── build.yml ← document template
|
|
129
|
-
├── templated.json ←
|
|
128
|
+
├── templated.json ← source of truth for templated config
|
|
130
129
|
└── build_node_component.yml ← job snippet template
|
|
131
130
|
```
|
|
132
131
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "enablement-build-template-generate",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Generates build yaml for mono repos",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./src/index.mjs",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"bin": "./src/index.mjs",
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"globby": "^16.2.0",
|
|
11
|
-
"user": "^0.0.0"
|
|
11
|
+
"user": "^0.0.0",
|
|
12
|
+
"enablement-build-manifest": "1.4.0"
|
|
12
13
|
},
|
|
13
14
|
"devDependencies": {
|
|
14
15
|
"jest": "^30.4.2"
|
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 {
|
|
@@ -15,6 +15,17 @@ import {
|
|
|
15
15
|
resolveOtherPlaceholders,
|
|
16
16
|
applyVarsToDocTemplate,
|
|
17
17
|
wrapWithBanner,
|
|
18
|
+
resolveWorkspaceChildren,
|
|
19
|
+
excludeChildren,
|
|
20
|
+
buildPackageJsonGlob,
|
|
21
|
+
detectCicdType,
|
|
22
|
+
isAdvancedTemplatedFormat,
|
|
23
|
+
TEMPLATED_JSON_ADVANCED_VERSION,
|
|
24
|
+
extractGenerationCicd,
|
|
25
|
+
collectAssignedPackages,
|
|
26
|
+
listBuildTemplateFiles,
|
|
27
|
+
chooseAssignmentForNewPackage,
|
|
28
|
+
topologicalSort,
|
|
18
29
|
} from './lib.mjs';
|
|
19
30
|
|
|
20
31
|
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
|
@@ -32,19 +43,54 @@ function readJobTemplate(templateFile, templatePath, fallbackTemplatePath) {
|
|
|
32
43
|
throw new Error(`Template file not found: ${templateFile} (looked in ${templatePath}${fallbackTemplatePath ? ' and ' + fallbackTemplatePath : ''})`);
|
|
33
44
|
}
|
|
34
45
|
|
|
46
|
+
|
|
47
|
+
// Resolve package folder name for {name}: prefer cicd.folderName, otherwise infer from disk.
|
|
48
|
+
function resolveFolderName(pkgName, info = {}) {
|
|
49
|
+
if (info.folderName) return info.folderName;
|
|
50
|
+
|
|
51
|
+
const packageFolder = info.packageFolder || 'packages';
|
|
52
|
+
const packageRoot = path.join(process.cwd(), packageFolder);
|
|
53
|
+
if (!existsSync(packageRoot)) return resolvePackageName(pkgName);
|
|
54
|
+
|
|
55
|
+
const dirs = readdirSync(packageRoot, { withFileTypes: true })
|
|
56
|
+
.filter(entry => entry.isDirectory())
|
|
57
|
+
.map(entry => entry.name);
|
|
58
|
+
|
|
59
|
+
for (const dirName of dirs) {
|
|
60
|
+
const base = path.join(packageRoot, dirName);
|
|
61
|
+
const candidates = ['package.json', 'version.json', 'pyproject.toml'];
|
|
62
|
+
|
|
63
|
+
for (const candidate of candidates) {
|
|
64
|
+
const manifestPath = path.join(base, candidate);
|
|
65
|
+
if (!existsSync(manifestPath)) continue;
|
|
66
|
+
|
|
67
|
+
const ext = path.extname(candidate).substring(1).toLowerCase();
|
|
68
|
+
const content = readFileSync(manifestPath, 'utf8');
|
|
69
|
+
const parsedName = parseManifestName(content, ext);
|
|
70
|
+
|
|
71
|
+
if (parsedName === pkgName) return dirName;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return resolvePackageName(pkgName);
|
|
76
|
+
}
|
|
77
|
+
|
|
35
78
|
// ---------------------------------------------------------------------------
|
|
36
79
|
// Expand using manifest.json "templated" section (new format)
|
|
37
80
|
// ---------------------------------------------------------------------------
|
|
38
|
-
function expandVarFromManifest(varname, packageMap, cicd, templatePath, fallbackTemplatePath, options) {
|
|
81
|
+
function expandVarFromManifest(varname, packageMap, cicd, templatePath, fallbackTemplatePath, options, dependencies = {}) {
|
|
39
82
|
return new Promise((resolve, reject) => {
|
|
40
83
|
try {
|
|
41
|
-
const
|
|
84
|
+
const filteredNames = Object.keys(packageMap).filter(pkg => {
|
|
42
85
|
if (packageMap[pkg] === false) return false;
|
|
43
|
-
const folderName = (cicd[pkg] || {})
|
|
86
|
+
const folderName = resolveFolderName(pkg, cicd[pkg] || {});
|
|
44
87
|
return !options.exclude.includes(folderName);
|
|
45
88
|
});
|
|
89
|
+
// Order so a package's local dependencies are generated before it,
|
|
90
|
+
// as much as possible (per manifest.json's "dependencies").
|
|
91
|
+
const packageNames = topologicalSort(filteredNames, dependencies);
|
|
46
92
|
|
|
47
|
-
const allFolderNames = packageNames.map(pkg => (cicd[pkg] || {})
|
|
93
|
+
const allFolderNames = packageNames.map(pkg => resolveFolderName(pkg, cicd[pkg] || {}));
|
|
48
94
|
|
|
49
95
|
const folderCounts = {};
|
|
50
96
|
packageNames.forEach(pkg => {
|
|
@@ -60,7 +106,7 @@ function expandVarFromManifest(varname, packageMap, cicd, templatePath, fallback
|
|
|
60
106
|
: [packageMap[pkgName]];
|
|
61
107
|
|
|
62
108
|
const info = cicd[pkgName] || {};
|
|
63
|
-
const name = info
|
|
109
|
+
const name = resolveFolderName(pkgName, info);
|
|
64
110
|
const jobRoot = info.packageFolder || 'packages';
|
|
65
111
|
const safe = makeSafe(name);
|
|
66
112
|
const packageName = resolvePackageName(pkgName);
|
|
@@ -81,86 +127,198 @@ function expandVarFromManifest(varname, packageMap, cicd, templatePath, fallback
|
|
|
81
127
|
}
|
|
82
128
|
|
|
83
129
|
// ---------------------------------------------------------------------------
|
|
84
|
-
//
|
|
130
|
+
// Convert one legacy templated.json entry ({ rootPath, template }) into the
|
|
131
|
+
// advanced format: discover the packages currently under rootPath and assign
|
|
132
|
+
// each of them the entry's template, recording their folder/packageFolder.
|
|
85
133
|
// ---------------------------------------------------------------------------
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
});
|
|
134
|
+
async function convertLegacyEntry(jobConfig, templatePath, options) {
|
|
135
|
+
const cwd = path.join(templatePath, jobConfig.rootPath);
|
|
136
|
+
const search = buildSearchGlobs(options.exclude);
|
|
137
|
+
const files = await globby(search, { cwd });
|
|
138
|
+
|
|
139
|
+
const folderBest = groupByFolder(files);
|
|
140
|
+
const allNames = Object.keys(folderBest).sort().filter(n => !options.exclude.includes(n));
|
|
141
|
+
const packageFolder = path.relative(process.cwd(), cwd);
|
|
142
|
+
|
|
143
|
+
const pkgMap = {};
|
|
144
|
+
const cicdEntries = {};
|
|
145
|
+
allNames.forEach(name => {
|
|
146
|
+
const content = readFileSync(path.join(cwd, folderBest[name].filePath), 'utf8');
|
|
147
|
+
const ext = path.extname(folderBest[name].filePath).substring(1).toLowerCase();
|
|
148
|
+
const fullName = parseManifestName(content, ext) || name;
|
|
149
|
+
pkgMap[fullName] = jobConfig.template;
|
|
150
|
+
cicdEntries[fullName] = { folderName: name, packageFolder };
|
|
151
|
+
});
|
|
105
152
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
153
|
+
return { pkgMap, cicdEntries };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Convert a whole legacy-format templated.json document into the advanced
|
|
157
|
+
// { templated, cicd } shape.
|
|
158
|
+
async function convertLegacyTemplatedJson(rawConfig, templatePath, options) {
|
|
159
|
+
const templated = {};
|
|
160
|
+
const cicd = {};
|
|
161
|
+
|
|
162
|
+
for (const pipelineFile of Object.keys(rawConfig)) {
|
|
163
|
+
templated[pipelineFile] = {};
|
|
164
|
+
for (const varname of Object.keys(rawConfig[pipelineFile])) {
|
|
165
|
+
const { pkgMap, cicdEntries } = await convertLegacyEntry(rawConfig[pipelineFile][varname], templatePath, options);
|
|
166
|
+
templated[pipelineFile][varname] = pkgMap;
|
|
167
|
+
Object.assign(cicd, cicdEntries);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { templated, cicd };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Discover packages not yet assigned to any template and add them, using
|
|
176
|
+
// detected package type + the existing convention (same packageFolder, else
|
|
177
|
+
// same output type, else the default "PACKAGES" placeholder). Mutates
|
|
178
|
+
// `templated`/`cicd` in place; returns true if anything was added.
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
async function detectAndAssignNewPackages(templated, cicd, options, templatePath) {
|
|
181
|
+
const assigned = collectAssignedPackages(templated);
|
|
182
|
+
const buildTemplateFiles = listBuildTemplateFiles(templatePath);
|
|
183
|
+
|
|
184
|
+
const pkgPaths = await globby(buildPackageJsonGlob(options.children), {
|
|
185
|
+
cwd: process.cwd(),
|
|
186
|
+
ignore: ['**/node_modules/**', '**/tests/**'],
|
|
187
|
+
absolute: true,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
let added = false;
|
|
191
|
+
|
|
192
|
+
pkgPaths.forEach(pkgPath => {
|
|
193
|
+
let pkg;
|
|
194
|
+
try { pkg = JSON.parse(readFileSync(pkgPath, 'utf8')); } catch { return; }
|
|
195
|
+
if (!pkg.name || !pkg.version || assigned.has(pkg.name)) return;
|
|
196
|
+
|
|
197
|
+
const folderName = path.basename(path.dirname(pkgPath));
|
|
198
|
+
if (options.exclude.includes(folderName)) return;
|
|
199
|
+
|
|
200
|
+
const packageFolder = path.basename(path.dirname(path.dirname(pkgPath)));
|
|
201
|
+
const detected = detectCicdType(pkg.name, pkg, pkgPath);
|
|
202
|
+
const { pipelineFile, placeholder, template } = chooseAssignmentForNewPackage({
|
|
203
|
+
packageFolder,
|
|
204
|
+
output: detected ? detected.output : undefined,
|
|
205
|
+
templated,
|
|
206
|
+
cicd,
|
|
207
|
+
buildTemplateFiles,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (!template) return;
|
|
211
|
+
|
|
212
|
+
templated[pipelineFile] = templated[pipelineFile] || {};
|
|
213
|
+
templated[pipelineFile][placeholder] = templated[pipelineFile][placeholder] || {};
|
|
214
|
+
templated[pipelineFile][placeholder][pkg.name] = template;
|
|
215
|
+
cicd[pkg.name] = { ...(cicd[pkg.name] || {}), folderName, packageFolder };
|
|
216
|
+
assigned.add(pkg.name);
|
|
217
|
+
added = true;
|
|
109
218
|
});
|
|
219
|
+
|
|
220
|
+
return added;
|
|
110
221
|
}
|
|
111
222
|
|
|
112
223
|
// ---------------------------------------------------------------------------
|
|
113
224
|
// main
|
|
114
225
|
// ---------------------------------------------------------------------------
|
|
115
226
|
async function main(options) {
|
|
227
|
+
// Detect workspace package roots (pnpm-workspace.yaml or package.json
|
|
228
|
+
// workspaces), same as monorepo-version and manifest. Used below to scope
|
|
229
|
+
// discovery of new (not-yet-configured) packages.
|
|
230
|
+
if (!options.children) {
|
|
231
|
+
const detected = resolveWorkspaceChildren(process.cwd());
|
|
232
|
+
if (detected) {
|
|
233
|
+
if (options.debug) console.log(`Auto-detected workspace children: ${detected}`);
|
|
234
|
+
options.children = detected;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// --exclude drops specific root directories from whichever children list is
|
|
239
|
+
// in effect, auto-detected or explicit. The same list is also used below to
|
|
240
|
+
// filter individual packages by folder name during generation.
|
|
241
|
+
if (options.exclude.length > 0) {
|
|
242
|
+
options.children = excludeChildren(options.children, options.exclude) || options.children;
|
|
243
|
+
}
|
|
244
|
+
|
|
116
245
|
const configDir = options.platform === 'github' ? '.github' : '.cicd';
|
|
117
246
|
const altConfigDir = options.platform === 'github' ? '.cicd' : '.github';
|
|
118
247
|
const templatePath = path.join(process.cwd(), `${configDir}/templates`);
|
|
119
248
|
const fallbackTemplatePath = path.join(process.cwd(), `${altConfigDir}/templates`);
|
|
120
249
|
|
|
121
|
-
// Try manifest.json first, then fall back to templated.json
|
|
122
250
|
const manifestPath = path.join(process.cwd(), `${configDir}/manifest.json`);
|
|
123
251
|
const templatedJsonPath = path.join(process.cwd(), `${configDir}/templates/templated.json`);
|
|
124
252
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
253
|
+
// ── Load config: manifest.json's "templated" section (if still present, to
|
|
254
|
+
// be migrated out), else templated.json (new or legacy format). Whatever we
|
|
255
|
+
// load is normalized into { templated, cicd } once, up front, so generation
|
|
256
|
+
// below always runs the same (advanced-format) code path.
|
|
257
|
+
let templated;
|
|
258
|
+
let cicd;
|
|
259
|
+
let manifestToPersist = null; // set when manifest.json needs its "templated" section stripped
|
|
260
|
+
let templatedJsonDirty = false; // set when templated.json needs (re)writing
|
|
261
|
+
let dependencies = {}; // manifest.json's local dependency graph, for ordering generation
|
|
262
|
+
|
|
263
|
+
if (existsSync(manifestPath)) {
|
|
264
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8') || '{}');
|
|
265
|
+
dependencies = manifest.dependencies || {};
|
|
266
|
+
if (isAdvancedTemplatedFormat(manifest)) {
|
|
267
|
+
templated = manifest.templated;
|
|
268
|
+
cicd = extractGenerationCicd(manifest.cicd || {});
|
|
269
|
+
delete manifest.templated;
|
|
270
|
+
manifestToPersist = manifest;
|
|
271
|
+
templatedJsonDirty = true;
|
|
272
|
+
}
|
|
130
273
|
}
|
|
131
274
|
|
|
132
|
-
|
|
133
|
-
|
|
275
|
+
if (!templated) {
|
|
276
|
+
if (!existsSync(templatedJsonPath)) {
|
|
277
|
+
throw `enablement-build-template-generate: could not find manifest.json "templated" section at ${configDir}/manifest.json or templated.json at ${configDir}/templates/templated.json`;
|
|
278
|
+
}
|
|
134
279
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
280
|
+
const raw = JSON.parse(readFileSync(templatedJsonPath, 'utf8') || '{}');
|
|
281
|
+
if (isAdvancedTemplatedFormat(raw)) {
|
|
282
|
+
templated = raw.templated;
|
|
283
|
+
cicd = raw.cicd || {};
|
|
284
|
+
if (typeof raw.version !== 'number') {
|
|
285
|
+
// Advanced format but predates the version field: write it back once,
|
|
286
|
+
// purely to backfill it, even if nothing else changed.
|
|
287
|
+
templatedJsonDirty = true;
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
const converted = await convertLegacyTemplatedJson(raw, templatePath, options);
|
|
291
|
+
templated = converted.templated;
|
|
292
|
+
cicd = converted.cicd;
|
|
293
|
+
templatedJsonDirty = true;
|
|
139
294
|
}
|
|
140
|
-
config = manifest.templated;
|
|
141
|
-
cicd = manifest.cicd || {};
|
|
142
|
-
} else {
|
|
143
|
-
config = JSON.parse(readFileSync(templatedJsonPath, 'utf8'));
|
|
144
295
|
}
|
|
145
296
|
|
|
297
|
+
// ── Discover packages not yet assigned to any template and add them.
|
|
298
|
+
if (await detectAndAssignNewPackages(templated, cicd, options, templatePath)) {
|
|
299
|
+
templatedJsonDirty = true;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── Persist. At most one write to each file, only when something changed.
|
|
303
|
+
if (manifestToPersist) {
|
|
304
|
+
writeFileSync(manifestPath, JSON.stringify(manifestToPersist, null, 2), 'utf8');
|
|
305
|
+
}
|
|
306
|
+
if (templatedJsonDirty) {
|
|
307
|
+
writeFileSync(templatedJsonPath, JSON.stringify({ version: TEMPLATED_JSON_ADVANCED_VERSION, templated, cicd }, null, 2), 'utf8');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── Generate.
|
|
146
311
|
const plist = [];
|
|
147
312
|
|
|
148
|
-
Object.keys(
|
|
313
|
+
Object.keys(templated).forEach((templateFilename) => {
|
|
149
314
|
plist.push(new Promise((primaryResolve, reject) => {
|
|
150
315
|
try {
|
|
151
316
|
const docTemplate = readFileSync(path.join(templatePath, templateFilename), 'utf8');
|
|
152
317
|
const jlist = [];
|
|
153
318
|
|
|
154
|
-
Object.keys(
|
|
155
|
-
const varConfig =
|
|
156
|
-
|
|
157
|
-
if (useManifest) {
|
|
158
|
-
// New format: varConfig is { pkgName: "template.yml" | [...] }
|
|
159
|
-
jlist.push(expandVarFromManifest(varname, varConfig, cicd, templatePath, fallbackTemplatePath, options));
|
|
160
|
-
} else {
|
|
161
|
-
// Legacy format: varConfig is { rootPath, template, exclude? }
|
|
162
|
-
jlist.push(expandVarFromTemplatedJson(varname, varConfig, templatePath, fallbackTemplatePath, options));
|
|
163
|
-
}
|
|
319
|
+
Object.keys(templated[templateFilename]).forEach((varname) => {
|
|
320
|
+
const varConfig = templated[templateFilename][varname];
|
|
321
|
+
jlist.push(expandVarFromManifest(varname, varConfig, cicd, templatePath, fallbackTemplatePath, options, dependencies));
|
|
164
322
|
});
|
|
165
323
|
|
|
166
324
|
Promise.all(jlist).then((results) => {
|
|
@@ -188,9 +346,10 @@ async function main(options) {
|
|
|
188
346
|
// CLI
|
|
189
347
|
// ---------------------------------------------------------------------------
|
|
190
348
|
let options = {
|
|
191
|
-
platform:
|
|
349
|
+
platform: 'ado',
|
|
192
350
|
exclude: [],
|
|
193
351
|
debug: false,
|
|
352
|
+
children: null,
|
|
194
353
|
};
|
|
195
354
|
|
|
196
355
|
let argv = process.argv;
|
|
@@ -211,20 +370,6 @@ for (let i = 2; i < argv.length; i++) {
|
|
|
211
370
|
}
|
|
212
371
|
}
|
|
213
372
|
|
|
214
|
-
if (!options.platform) {
|
|
215
|
-
const hasGithubManifest = existsSync(path.join(process.cwd(), '.github/manifest.json'));
|
|
216
|
-
const hasAdoManifest = existsSync(path.join(process.cwd(), '.cicd/manifest.json'));
|
|
217
|
-
const hasGithubTemplated = existsSync(path.join(process.cwd(), '.github/templates/templated.json'));
|
|
218
|
-
const hasAdoTemplated = existsSync(path.join(process.cwd(), '.cicd/templates/templated.json'));
|
|
219
|
-
|
|
220
|
-
if (hasGithubManifest || hasGithubTemplated) {
|
|
221
|
-
options.platform = 'github';
|
|
222
|
-
} else if (hasAdoManifest || hasAdoTemplated) {
|
|
223
|
-
options.platform = 'ado';
|
|
224
|
-
} else {
|
|
225
|
-
options.platform = 'ado';
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
373
|
|
|
229
374
|
(async () => {
|
|
230
375
|
try {
|