Skip to content

Commit 33d85e4

Browse files
author
Pablo Mendez
committed
validate files props
1 parent cce36b6 commit 33d85e4

11 files changed

Lines changed: 205 additions & 52 deletions

File tree

src/files/manifest/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export { getRepoSlugFromManifest } from "./getRepoSlugFromManifest.js";
44
export { readManifest } from "./readManifest.js";
55
export { writeManifest } from "./writeManifest.js";
66
export { stringifyJson } from "./stringifyJson.js";
7+
export { ManifestFormat, ManifestPaths } from "./types.js";

src/files/notifications/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { readNotificationsIfExists } from "./readNotificationsIfExists.js";
2+
export { NotificationsFormat, NotificationsPaths } from "./types.js";

src/files/notifications/readNotificationsIfExists.ts

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,63 @@ import path from "path";
33
import yaml from "js-yaml";
44
import { defaultDir } from "../../params.js";
55
import { readFile } from "../../utils/file.js";
6-
import { NotificationsConfig, releaseFiles} from "@dappnode/types";
6+
import { NotificationsConfig, releaseFiles } from "@dappnode/types";
7+
import { merge } from "lodash-es";
8+
import { NotificationsPaths } from "./types.js";
79

8-
export function readNotificationsIfExists(dir?: string): NotificationsConfig | null {
9-
const dirPath = dir || defaultDir;
10-
const notificationsFileName = fs
11-
.readdirSync(dirPath)
12-
.find(file => releaseFiles.notifications.regex.test(file));
10+
/**
11+
* Reads one or multiple notifications YAML files and merges them. Returns null if none exist.
12+
* @param {NotificationsPaths[]} paths - Optional array of directory/file specs.
13+
* @returns {NotificationsConfig | null}
14+
* @throws {Error} Throws if parsing fails or non-YAML format is encountered.
15+
*/
16+
export function readNotificationsIfExists(
17+
paths?: NotificationsPaths[]
18+
): NotificationsConfig | null {
19+
try {
20+
// Determine list of specs (default single spec if none provided)
21+
const specs = paths && paths.length > 0 ? paths : [{}];
1322

14-
if (!notificationsFileName) return null;
15-
const data = readFile(path.join(dirPath, notificationsFileName));
23+
// Resolve existing file paths
24+
const filePaths = specs
25+
.map(spec => {
26+
try {
27+
return findNotificationsPath(spec);
28+
} catch {
29+
return undefined;
30+
}
31+
})
32+
.filter((p): p is string => typeof p === "string");
1633

17-
// Parse notifications in try catch block to show a comprehensive error message
18-
try {
19-
return yaml.load(data);
34+
if (filePaths.length === 0) return null;
35+
36+
// Load and validate YAML-only files
37+
const configs = filePaths.map(fp => {
38+
if (!/\.(yml|yaml)$/i.test(fp))
39+
throw new Error("Only YAML format supported for notifications: " + fp);
40+
const data = readFile(fp);
41+
const parsed = yaml.load(data);
42+
if (!parsed || typeof parsed === "string")
43+
throw new Error(`Could not parse notifications: ${fp}`);
44+
return parsed as NotificationsConfig;
45+
});
46+
47+
// Merge all configurations
48+
return merge({}, ...configs);
2049
} catch (e) {
21-
throw Error(`Error parsing notifications: ${e.message}`);
50+
throw new Error(`Error parsing notifications: ${e.message}`);
51+
}
52+
}
53+
54+
// Find a notifications file, throws if not found
55+
function findNotificationsPath(spec?: NotificationsPaths): string {
56+
const dirPath = spec?.dir || defaultDir;
57+
if (spec?.notificationsFileName) {
58+
return path.join(dirPath, spec.notificationsFileName);
2259
}
60+
const files: string[] = fs.readdirSync(dirPath);
61+
const match = files.find(f => releaseFiles.notifications.regex.test(f));
62+
if (!match)
63+
throw new Error(`No notifications file found in directory ${dirPath}`);
64+
return path.join(dirPath, match);
2365
}

src/files/notifications/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export enum NotificationsFormat {
2+
json = "json",
3+
yml = "yml",
4+
yaml = "yaml"
5+
}
6+
7+
export interface NotificationsPaths {
8+
/** './folder', [optional] directory to load the notifications from */
9+
dir?: string;
10+
/** 'notifications-admin.json', [optional] name of the notifications file */
11+
notificationsFileName?: string;
12+
}

src/files/setupWizard/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { readSetupWizardIfExists } from "./readSetupWizardIfExists.js";
2+
export { SetupWizardFormat, SetupWizardPaths } from "./types.js";
Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,64 @@
11
import fs from "fs";
22
import path from "path";
33
import yaml from "js-yaml";
4+
import { merge } from "lodash-es";
45
import { defaultDir } from "../../params.js";
56
import { readFile } from "../../utils/file.js";
67
import { SetupWizard, releaseFiles } from "@dappnode/types";
8+
import { SetupWizardPaths } from "./types.js";
79

8-
export function readSetupWizardIfExists(dir?: string): SetupWizard | null {
9-
const dirPath = dir || defaultDir;
10-
const setupWizardFileName = fs
11-
.readdirSync(dirPath)
12-
.find(file => releaseFiles.setupWizard.regex.test(file));
10+
/**
11+
* Reads one or multiple setup-wizard YAML files and merges them. Returns null if none exist.
12+
* @param {SetupWizardPaths[]} paths - Optional array of directory/file specs.
13+
* @returns {SetupWizard | null}
14+
* @throws {Error} Throws if parsing fails or non-YAML format is encountered.
15+
*/
16+
export function readSetupWizardIfExists(
17+
paths?: SetupWizardPaths[]
18+
): SetupWizard | null {
19+
try {
20+
// Determine list of file specs (default spec if no paths provided)
21+
const specs = paths && paths.length > 0 ? paths : [{}];
1322

14-
if (!setupWizardFileName) return null;
15-
const data = readFile(path.join(dirPath, setupWizardFileName));
23+
// Resolve existing file paths
24+
const filePaths = specs
25+
.map(spec => {
26+
try {
27+
return findSetupWizardPath(spec);
28+
} catch {
29+
return undefined;
30+
}
31+
})
32+
.filter((p): p is string => typeof p === "string");
1633

17-
// Parse setupWizard in try catch block to show a comprehensive error message
18-
try {
19-
return yaml.load(data);
34+
if (filePaths.length === 0) return null;
35+
36+
// Load and validate YAML-only files
37+
const wizards = filePaths.map(fp => {
38+
if (!/\.(yml|yaml)$/i.test(fp))
39+
throw new Error("Only YAML format supported for setup-wizard: " + fp);
40+
const data = readFile(fp);
41+
const parsed = yaml.load(data);
42+
if (!parsed || typeof parsed === "string")
43+
throw new Error(`Could not parse setup-wizard: ${fp}`);
44+
return parsed as SetupWizard;
45+
});
46+
47+
// Merge all specs
48+
return merge({}, ...wizards);
2049
} catch (e) {
21-
throw Error(`Error parsing setup-wizard: ${e.message}`);
50+
throw new Error(`Error parsing setup-wizard: ${e.message}`);
2251
}
2352
}
53+
54+
// Find a setup-wizard file, throws if not found
55+
function findSetupWizardPath(spec?: SetupWizardPaths): string {
56+
const dirPath = spec?.dir || defaultDir;
57+
if (spec?.setupWizardFileName)
58+
return path.join(dirPath, spec.setupWizardFileName);
59+
const files: string[] = fs.readdirSync(dirPath);
60+
const match = files.find(f => releaseFiles.setupWizard.regex.test(f));
61+
if (!match)
62+
throw new Error(`No setup-wizard file found in directory ${dirPath}`);
63+
return path.join(dirPath, match);
64+
}

src/files/setupWizard/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export enum SetupWizardFormat {
2+
json = "json",
3+
yml = "yml",
4+
yaml = "yaml"
5+
}
6+
7+
export interface SetupWizardPaths {
8+
/** './folder', [optional] directory to load the setup-wizard from */
9+
dir?: string;
10+
/** 'setup-wizard-admin.json', [optional] name of the setup-wizard file */
11+
setupWizardFileName?: string;
12+
}

src/tasks/buildAndUpload/generatePackagesProps.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import {
44
readCompose,
55
getComposePackageImages,
66
parseComposeUpstreamVersion,
7-
readManifest
7+
readManifest,
8+
readNotificationsIfExists,
9+
readSetupWizardIfExists
810
} from "../../files/index.js";
911
import { Compose, Manifest } from "@dappnode/types";
1012
import { defaultComposeFileName } from "../../params.js";
@@ -22,9 +24,16 @@ export function generatePackagesProps({
2224
composeFileName?: string;
2325
}): PackageToBuildProps[] {
2426
if (variants === null)
25-
return [createPackagePropsItem({ rootDir, composeFileName, variant: null, variantsDirPath })];
26-
27-
return variants.map((variant) =>
27+
return [
28+
createPackagePropsItem({
29+
rootDir,
30+
composeFileName,
31+
variant: null,
32+
variantsDirPath
33+
})
34+
];
35+
36+
return variants.map(variant =>
2837
createPackagePropsItem({
2938
rootDir,
3039
composeFileName,
@@ -47,16 +56,22 @@ function createPackagePropsItem({
4756
}): PackageToBuildProps {
4857
const manifestPaths = [{ dir: rootDir }];
4958
const composePaths = [{ dir: rootDir, composeFileName }];
59+
const notificationsPaths = [{ dir: rootDir }];
60+
const setupWizardPaths = [{ dir: rootDir }];
5061

5162
if (variant) {
5263
const variantPath = path.join(variantsDirPath, variant);
5364

5465
manifestPaths.push({ dir: variantPath });
5566
composePaths.push({ dir: variantPath, composeFileName });
67+
notificationsPaths.push({ dir: variantPath });
68+
setupWizardPaths.push({ dir: variantPath });
5669
}
5770

5871
const { manifest, format: manifestFormat } = readManifest(manifestPaths);
5972
const compose = readCompose(composePaths);
73+
const notifications = readNotificationsIfExists(notificationsPaths);
74+
const setupWizard = readSetupWizardIfExists(setupWizardPaths);
6075

6176
const { name: dnpName, version } = manifest;
6277

@@ -66,18 +81,17 @@ function createPackagePropsItem({
6681

6782
return {
6883
variant,
69-
7084
manifest,
7185
manifestFormat,
72-
7386
compose,
74-
87+
notifications,
88+
setupWizard,
7589
releaseDir: getReleaseDirPath({ rootDir, dnpName, version }),
7690
manifestPaths,
7791
composePaths,
78-
92+
notificationsPaths,
93+
setupWizardPaths,
7994
images: getComposePackageImages(compose, manifest),
80-
8195
architectures: parseArchitectures({
8296
rawArchs: manifest.architectures
8397
})

src/tasks/buildAndUpload/getFileValidationTask.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,43 +7,39 @@ import {
77
validateDappnodeCompose,
88
validateNotificationsSchema
99
} from "@dappnode/schemas";
10-
import { getComposePath, readNotificationsIfExists, readSetupWizardIfExists } from "../../files/index.js";
10+
import { getComposePath } from "../../files/index.js";
1111
import { CliError } from "../../params.js";
1212

1313
export function getFileValidationTask({
14-
packagesToBuildProps,
15-
rootDir
14+
packagesToBuildProps
1615
}: {
1716
packagesToBuildProps: PackageToBuildProps[];
18-
rootDir: string;
1917
}): ListrTask<ListrContextBuild> {
2018
return {
2119
title: `Validate files`,
22-
task: async () => await validatePackageFiles({ packagesToBuildProps, rootDir })
20+
task: async () => await validatePackageFiles({ packagesToBuildProps })
2321
};
2422
}
2523

2624
async function validatePackageFiles({
27-
packagesToBuildProps,
28-
rootDir
25+
packagesToBuildProps
2926
}: {
3027
packagesToBuildProps: PackageToBuildProps[];
31-
rootDir: string;
3228
}): Promise<void> {
33-
const setupWizard = readSetupWizardIfExists(rootDir);
34-
if (setupWizard) validateSetupWizardSchema(setupWizard);
35-
36-
const notifications = readNotificationsIfExists(rootDir);
37-
if (notifications) validateNotificationsSchema(notifications);
38-
3929
for (const pkgProps of packagesToBuildProps)
4030
await validateVariantFiles(pkgProps);
4131
}
4232

4333
async function validateVariantFiles(
4434
pkgProps: PackageToBuildProps
4535
): Promise<void> {
46-
const { manifest, compose, composePaths } = pkgProps;
36+
const {
37+
manifest,
38+
compose,
39+
composePaths,
40+
notifications,
41+
setupWizard
42+
} = pkgProps;
4743

4844
console.log(
4945
`Validating files for ${manifest.name} (version ${manifest.version})`
@@ -56,10 +52,18 @@ async function validateVariantFiles(
5652
validatePackageVersion(manifest.version);
5753

5854
// Validate compose file using docker compose
59-
await validateComposeSchema(composePaths.map(pathObj => getComposePath(pathObj)));
55+
await validateComposeSchema(
56+
composePaths.map(pathObj => getComposePath(pathObj))
57+
);
6058

6159
// Validate compose file specifically for Dappnode requirements
6260
validateDappnodeCompose(compose, manifest);
61+
62+
// Validate notifications schema
63+
if (notifications) validateNotificationsSchema(notifications);
64+
65+
// Validate setup wizard schema
66+
if (setupWizard) validateSetupWizardSchema(setupWizard);
6367
}
6468

6569
function validatePackageName(name: string): void {

src/tasks/buildAndUpload/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export function buildAndUpload({
3939
const releaseUploader = getReleaseUploader(releaseUploaderProvider);
4040

4141
return [
42-
getFileValidationTask({ packagesToBuildProps, rootDir: dir }),
42+
getFileValidationTask({ packagesToBuildProps }),
4343
getVerifyConnectionTask({ releaseUploader, skipUpload }),
4444
getReleaseDirCreationTask({ packagesToBuildProps }),
4545
getFileCopyTask({
@@ -49,7 +49,12 @@ export function buildAndUpload({
4949
composeFileName,
5050
requireGitData
5151
}),
52-
...getBuildTasks({ packagesToBuildProps, buildTimeout, skipSave, rootDir: dir }),
52+
...getBuildTasks({
53+
packagesToBuildProps,
54+
buildTimeout,
55+
skipSave,
56+
rootDir: dir
57+
}),
5358
...getUploadTasks({
5459
packagesToBuildProps,
5560
releaseUploader,

0 commit comments

Comments
 (0)