Procházet zdrojové kódy

feat: create app update event

Nicolas Meienberger před 1 rokem
rodič
revize
56c9d51d13

+ 6 - 0
.github/workflows/ci.yml

@@ -62,6 +62,12 @@ jobs:
       - name: Install dependencies
       - name: Install dependencies
         run: pnpm install
         run: pnpm install
 
 
+      - name: Run linter
+        run: pnpm run lint
+
+      - name: Run linter on packages
+        run: pnpm -r run lint
+
       - name: Get number of CPU cores
       - name: Get number of CPU cores
         id: cpu-cores
         id: cpu-cores
         uses: SimenB/github-actions-cpu-cores@v1
         uses: SimenB/github-actions-cpu-cores@v1

+ 10 - 10
packages/cli/assets/docker-compose.yml

@@ -11,8 +11,8 @@ services:
     command: --providers.docker
     command: --providers.docker
     volumes:
     volumes:
       - /var/run/docker.sock:/var/run/docker.sock:ro
       - /var/run/docker.sock:/var/run/docker.sock:ro
-      - ${PWD}/traefik:/root/.config
-      - ${PWD}/traefik/shared:/shared
+      - ./traefik:/root/.config
+      - ./traefik/shared:/shared
     networks:
     networks:
       - tipi_main_network
       - tipi_main_network
 
 
@@ -22,7 +22,7 @@ services:
     restart: on-failure
     restart: on-failure
     stop_grace_period: 1m
     stop_grace_period: 1m
     volumes:
     volumes:
-      - ${PWD}/data/postgres:/var/lib/postgresql/data
+      - ./data/postgres:/var/lib/postgresql/data
     environment:
     environment:
       POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
       POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
       POSTGRES_USER: tipi
       POSTGRES_USER: tipi
@@ -65,14 +65,14 @@ services:
     env_file:
     env_file:
       - .env
       - .env
     environment:
     environment:
-      NODE_ENV: development
+      NODE_ENV: production
     volumes:
     volumes:
-      - ${PWD}/.env:/runtipi/.env
-      - ${PWD}/state:/runtipi/state
-      - ${PWD}/repos:/runtipi/repos:ro
-      - ${PWD}/apps:/runtipi/apps
-      - ${PWD}/logs:/app/logs
-      - ${PWD}/traefik:/runtipi/traefik
+      - ./.env:/runtipi/.env
+      - ./state:/runtipi/state
+      - ./repos:/runtipi/repos:ro
+      - ./apps:/runtipi/apps
+      - ./logs:/app/logs
+      - ./traefik:/runtipi/traefik
       - ${STORAGE_PATH}:/app/storage
       - ${STORAGE_PATH}:/app/storage
     labels:
     labels:
       # Main
       # Main

+ 1 - 1
packages/cli/build.js

@@ -8,7 +8,7 @@ async function bundle() {
     entryPoints: ['./src/index.ts'],
     entryPoints: ['./src/index.ts'],
     outfile: './dist/index.js',
     outfile: './dist/index.js',
     platform: 'node',
     platform: 'node',
-    target: 'node20',
+    target: 'node18',
     bundle: true,
     bundle: true,
     color: true,
     color: true,
     sourcemap: commandArgs.includes('--sourcemap'),
     sourcemap: commandArgs.includes('--sourcemap'),

+ 2 - 1
packages/cli/package.json

@@ -13,7 +13,8 @@
     "build": "node build.js",
     "build": "node build.js",
     "build:meta": "esbuild ./src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --metafile=meta.json --analyze",
     "build:meta": "esbuild ./src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --metafile=meta.json --analyze",
     "dev": "dotenv -e ../../.env nodemon",
     "dev": "dotenv -e ../../.env nodemon",
-    "lint": "eslint . --ext .ts"
+    "lint": "eslint . --ext .ts",
+    "tsc": "tsc --noEmit"
   },
   },
   "pkg": {
   "pkg": {
     "assets": "assets/**/*",
     "assets": "assets/**/*",

+ 2 - 2
packages/cli/src/executors/app/app.helpers.ts

@@ -1,9 +1,9 @@
 import crypto from 'crypto';
 import crypto from 'crypto';
 import fs from 'fs';
 import fs from 'fs';
 import path from 'path';
 import path from 'path';
-import { appInfoSchema } from '@runtipi/shared';
+import { appInfoSchema, envMapToString, envStringToMap } from '@runtipi/shared';
 import { getEnv } from '@/utils/environment/environment';
 import { getEnv } from '@/utils/environment/environment';
-import { envMapToString, envStringToMap, generateVapidKeys, getAppEnvMap } from './env.helpers';
+import { generateVapidKeys, getAppEnvMap } from './env.helpers';
 import { pathExists } from '@/utils/fs-helpers';
 import { pathExists } from '@/utils/fs-helpers';
 
 
 /**
 /**

+ 0 - 30
packages/cli/src/executors/app/env.helpers.ts

@@ -3,36 +3,6 @@ import fs from 'fs';
 import path from 'path';
 import path from 'path';
 import { getEnv } from '@/utils/environment/environment';
 import { getEnv } from '@/utils/environment/environment';
 
 
-/**
- * Convert a string of environment variables to a Map
- *
- * @param {string} envString - String of environment variables
- */
-export const envStringToMap = (envString: string) => {
-  const envMap = new Map<string, string>();
-  const envArray = envString.split('\n');
-
-  envArray.forEach((env) => {
-    const [key, value] = env.split('=');
-    if (key && value) {
-      envMap.set(key, value);
-    }
-  });
-
-  return envMap;
-};
-
-/**
- * Convert a Map of environment variables to a valid string of environment variables
- * that can be used in a .env file
- *
- * @param {Map<string, string>} envMap - Map of environment variables
- */
-export const envMapToString = (envMap: Map<string, string>) => {
-  const envArray = Array.from(envMap).map(([key, value]) => `${key}=${value}`);
-  return envArray.join('\n');
-};
-
 /**
 /**
  * This function reads the env file for the app with the provided id and returns a Map containing the key-value pairs of the environment variables.
  * This function reads the env file for the app with the provided id and returns a Map containing the key-value pairs of the environment variables.
  * It reads the app.env file, splits it into individual environment variables, and stores them in a Map, with the environment variable name as the key and its value as the value.
  * It reads the app.env file, splits it into individual environment variables, and stores them in a Map, with the environment variable name as the key and its value as the value.

+ 5 - 1
packages/cli/src/services/watcher/watcher.ts

@@ -9,7 +9,7 @@ const execAsync = promisify(exec);
 const runCommand = async (jobData: unknown) => {
 const runCommand = async (jobData: unknown) => {
   const { installApp, startApp, stopApp, uninstallApp, updateApp, regenerateAppEnv } = new AppExecutors();
   const { installApp, startApp, stopApp, uninstallApp, updateApp, regenerateAppEnv } = new AppExecutors();
   const { cloneRepo, pullRepo } = new RepoExecutors();
   const { cloneRepo, pullRepo } = new RepoExecutors();
-  const { systemInfo, restart } = new SystemExecutors();
+  const { systemInfo, restart, update } = new SystemExecutors();
 
 
   const event = eventSchema.safeParse(jobData);
   const event = eventSchema.safeParse(jobData);
 
 
@@ -62,6 +62,10 @@ const runCommand = async (jobData: unknown) => {
     if (data.command === 'restart') {
     if (data.command === 'restart') {
       ({ success, message } = await restart());
       ({ success, message } = await restart());
     }
     }
+
+    if (data.command === 'update') {
+      ({ success, message } = await update(data.version));
+    }
   }
   }
 
 
   return { success, message };
   return { success, message };

+ 1 - 0
packages/shared/.eslintignore

@@ -0,0 +1 @@
+.eslintrc.js

+ 39 - 0
packages/shared/.eslintrc.js

@@ -0,0 +1,39 @@
+module.exports = {
+  root: true,
+  plugins: ['@typescript-eslint', 'import'],
+  extends: ['plugin:@typescript-eslint/recommended', 'airbnb', 'airbnb-typescript', 'eslint:recommended', 'plugin:import/typescript', 'prettier'],
+  parser: '@typescript-eslint/parser',
+  parserOptions: {
+    ecmaVersion: 'latest',
+    sourceType: 'module',
+    project: './tsconfig.json',
+    tsconfigRootDir: __dirname,
+  },
+  rules: {
+    'import/prefer-default-export': 0,
+    'class-methods-use-this': 0,
+    'import/extensions': [
+      'error',
+      'ignorePackages',
+      {
+        '': 'never',
+        js: 'never',
+        jsx: 'never',
+        ts: 'never',
+        tsx: 'never',
+      },
+    ],
+    'import/no-extraneous-dependencies': [
+      'error',
+      {
+        devDependencies: ['build.js', '**/*.test.{ts,tsx}', '**/mocks/**', '**/__mocks__/**', '**/*.setup.{ts,js}', '**/*.config.{ts,js}', '**/tests/**'],
+      },
+    ],
+    'arrow-body-style': 0,
+    'no-underscore-dangle': 0,
+    'no-console': 0,
+  },
+  globals: {
+    NodeJS: true,
+  },
+};

+ 4 - 1
packages/shared/package.json

@@ -3,7 +3,10 @@
   "version": "1.0.0",
   "version": "1.0.0",
   "description": "",
   "description": "",
   "main": "src/index.ts",
   "main": "src/index.ts",
-  "scripts": {},
+  "scripts": {
+    "lint": "eslint --ext .ts src",
+    "tsc": "tsc --noEmit"
+  },
   "keywords": [],
   "keywords": [],
   "author": "",
   "author": "",
   "license": "ISC",
   "license": "ISC",

+ 8 - 2
packages/shared/src/schemas/queue-schemas.ts

@@ -23,10 +23,16 @@ const repoCommandSchema = z.object({
 
 
 const systemCommandSchema = z.object({
 const systemCommandSchema = z.object({
   type: z.literal(EVENT_TYPES.SYSTEM),
   type: z.literal(EVENT_TYPES.SYSTEM),
-  command: z.union([z.literal('restart'), z.literal('update'), z.literal('system_info')]),
+  command: z.union([z.literal('restart'), z.literal('system_info')]),
 });
 });
 
 
-export const eventSchema = appCommandSchema.or(repoCommandSchema).or(systemCommandSchema);
+const updateSchema = z.object({
+  type: z.literal(EVENT_TYPES.SYSTEM),
+  command: z.literal('update'),
+  version: z.string(),
+});
+
+export const eventSchema = appCommandSchema.or(repoCommandSchema).or(systemCommandSchema).or(updateSchema);
 
 
 export const eventResultSchema = z.object({
 export const eventResultSchema = z.object({
   success: z.boolean(),
   success: z.boolean(),

+ 0 - 1
packages/shared/tsconfig.json

@@ -19,7 +19,6 @@
     "resolveJsonModule": true,
     "resolveJsonModule": true,
     "isolatedModules": true,
     "isolatedModules": true,
     "jsx": "preserve",
     "jsx": "preserve",
-    "incremental": true,
     "strictNullChecks": true,
     "strictNullChecks": true,
     "allowSyntheticDefaultImports": true,
     "allowSyntheticDefaultImports": true,
     "noUncheckedIndexedAccess": true,
     "noUncheckedIndexedAccess": true,

+ 0 - 40
src/server/services/apps/apps.helpers.ts

@@ -1,6 +1,4 @@
-import fs from 'fs-extra';
 import { App } from '@/server/db/schema';
 import { App } from '@/server/db/schema';
-import { getAppEnvMap } from '@/server/utils/env-generation';
 import { appInfoSchema } from '@runtipi/shared';
 import { appInfoSchema } from '@runtipi/shared';
 import { fileExists, readdirSync, readFile, readJsonFile } from '../../common/fs.helpers';
 import { fileExists, readdirSync, readFile, readJsonFile } from '../../common/fs.helpers';
 import { getConfig } from '../../core/TipiConfig';
 import { getConfig } from '../../core/TipiConfig';
@@ -32,44 +30,6 @@ export const checkAppRequirements = (appName: string) => {
   return parsedConfig.data;
   return parsedConfig.data;
 };
 };
 
 
-/**
- *  This function checks if the env file for the app with the provided name is valid.
- *  It reads the config.json file for the app, parses it,
- *  and uses the app's form fields to check if all required fields are present in the env file.
- *  If the config.json file is invalid, it throws an error.
- *  If a required variable is missing in the env file, it throws an error.
- *
- *  @param {string} appName - The name of the app.
- *  @throws Will throw an error if the app has an invalid config.json file or if a required variable is missing in the env file.
- */
-export const checkEnvFile = async (appName: string) => {
-  const configFile = await fs.promises.readFile(`/runtipi/apps/${appName}/config.json`);
-
-  let jsonConfig: unknown;
-  try {
-    jsonConfig = JSON.parse(configFile.toString());
-  } catch (e) {
-    throw new Error(`App ${appName} has invalid config.json file`);
-  }
-
-  const parsedConfig = appInfoSchema.safeParse(jsonConfig);
-
-  if (!parsedConfig.success) {
-    throw new Error(`App ${appName} has invalid config.json file`);
-  }
-
-  const envMap = await getAppEnvMap(appName);
-
-  parsedConfig.data.form_fields.forEach((field) => {
-    const envVar = field.env_variable;
-    const envVarValue = envMap.get(envVar);
-
-    if (!envVarValue && field.required) {
-      throw new Error('New info needed. App config needs to be updated');
-    }
-  });
-};
-
 /**
 /**
   This function reads the apps directory and skips certain system files, then reads the config.json and metadata/description.md files for each app,
   This function reads the apps directory and skips certain system files, then reads the config.json and metadata/description.md files for each app,
   parses the config file, filters out any apps that are not available and returns an array of app information.
   parses the config file, filters out any apps that are not available and returns an array of app information.

+ 5 - 5
src/server/services/system/system.service.ts

@@ -48,16 +48,16 @@ export class SystemServiceClass {
       let body = await this.cache.get('latestVersionBody');
       let body = await this.cache.get('latestVersionBody');
 
 
       if (!version) {
       if (!version) {
-        const { data } = await axios.get<{ name: string; body: string }>('https://api.github.com/repos/meienberger/runtipi/releases/latest');
+        const { data } = await axios.get<{ tag_name: string; body: string }>('https://api.github.com/repos/meienberger/runtipi/releases/latest');
 
 
-        version = data.name.replace('v', '');
+        version = data.tag_name;
         body = data.body;
         body = data.body;
 
 
-        await this.cache.set('latestVersion', version?.replace('v', '') || '', 60 * 60);
+        await this.cache.set('latestVersion', version || '', 60 * 60);
         await this.cache.set('latestVersionBody', body || '', 60 * 60);
         await this.cache.set('latestVersionBody', body || '', 60 * 60);
       }
       }
 
 
-      return { current: TipiConfig.getConfig().version, latest: version?.replace('v', ''), body };
+      return { current: TipiConfig.getConfig().version, latest: version, body };
     } catch (e) {
     } catch (e) {
       Logger.error(e);
       Logger.error(e);
       return { current: TipiConfig.getConfig().version, latest: undefined };
       return { current: TipiConfig.getConfig().version, latest: undefined };
@@ -101,7 +101,7 @@ export class SystemServiceClass {
 
 
     TipiConfig.setConfig('status', 'UPDATING');
     TipiConfig.setConfig('status', 'UPDATING');
 
 
-    this.dispatcher.dispatchEvent({ type: 'system', command: 'update' });
+    this.dispatcher.dispatchEvent({ type: 'system', command: 'update', version: latest });
 
 
     return true;
     return true;
   };
   };