Skip to content

Commit cfc935d

Browse files
pablomendezroyoPablo MendezMarketen
authored
Implement notifications release file (#453)
* copy notifications file in variants dir * remove unnecessary changes * validate notifications file * validate files props * fix buildVariantMap test --------- Co-authored-by: Pablo Mendez <pablo@dappnode.io> Co-authored-by: Marketen <marcfont12@gmail.com>
1 parent e683102 commit cfc935d

14 files changed

Lines changed: 235 additions & 40 deletions

File tree

src/files/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from "./compose/index.js";
22
export * from "./manifest/index.js";
33
export * from "./setupWizard/index.js";
4+
export * from "./notifications/index.js";

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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { readNotificationsIfExists } from "./readNotificationsIfExists.js";
2+
export { NotificationsFormat, NotificationsPaths } from "./types.js";
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import fs from "fs";
2+
import path from "path";
3+
import yaml from "js-yaml";
4+
import { defaultDir } from "../../params.js";
5+
import { readFile } from "../../utils/file.js";
6+
import { NotificationsConfig, releaseFiles } from "@dappnode/types";
7+
import { merge } from "lodash-es";
8+
import { NotificationsPaths } from "./types.js";
9+
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 : [{}];
22+
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");
33+
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);
49+
} catch (e) {
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);
59+
}
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);
65+
}

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/getFileCopyTask.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,17 @@ async function copyVariantFilesToReleaseDir({
111111
}
112112
break;
113113

114+
case "notifications":
115+
// Copy the notifications in root and in the variant dir
116+
for (const dir of dirsToCopy) {
117+
copyReleaseFile({
118+
fileConfig: { ...fileConfig, id: fileId },
119+
fromDir: dir,
120+
toDir: releaseDir
121+
});
122+
}
123+
break;
124+
114125
default:
115126
copyReleaseFile({
116127
fileConfig: { ...fileConfig, id: fileId },

0 commit comments

Comments
 (0)