Kaynağa Gözat

feat: create app update event

Nicolas Meienberger 1 yıl önce
ebeveyn
işleme
56c9d51d13

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

@@ -62,6 +62,12 @@ jobs:
       - name: Install dependencies
         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
         id: cpu-cores
         uses: SimenB/github-actions-cpu-cores@v1

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

@@ -11,8 +11,8 @@ services:
     command: --providers.docker
     volumes:
       - /var/run/docker.sock:/var/run/docker.sock:ro
-      - ${PWD}/traefik:/root/.config
-      - ${PWD}/traefik/shared:/shared
+      - ./traefik:/root/.config
+      - ./traefik/shared:/shared
     networks:
       - tipi_main_network
 
@@ -22,7 +22,7 @@ services:
     restart: on-failure
     stop_grace_period: 1m
     volumes:
-      - ${PWD}/data/postgres:/var/lib/postgresql/data
+      - ./data/postgres:/var/lib/postgresql/data
     environment:
       POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
       POSTGRES_USER: tipi
@@ -65,14 +65,14 @@ services:
     env_file:
       - .env
     environment:
-      NODE_ENV: development
+      NODE_ENV: production
     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
     labels:
       # Main

+ 1 - 1
packages/cli/build.js

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

+ 2 - 1
packages/cli/package.json

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

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

@@ -1,9 +1,9 @@
 import crypto from 'crypto';
 import fs from 'fs';
 import path from 'path';
-import { appInfoSchema } from '@runtipi/shared';
+import { appInfoSchema, envMapToString, envStringToMap } from '@runtipi/shared';
 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';
 
 /**

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

@@ -3,36 +3,6 @@ import fs from 'fs';
 import path from 'path';
 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.
  * 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 { installApp, startApp, stopApp, uninstallApp, updateApp, regenerateAppEnv } = new AppExecutors();
   const { cloneRepo, pullRepo } = new RepoExecutors();
-  const { systemInfo, restart } = new SystemExecutors();
+  const { systemInfo, restart, update } = new SystemExecutors();
 
   const event = eventSchema.safeParse(jobData);
 
@@ -62,6 +62,10 @@ const runCommand = async (jobData: unknown) => {
     if (data.command === 'restart') {
       ({ success, message } = await restart());
     }
+
+    if (data.command === 'update') {
+      ({ success, message } = await update(data.version));
+    }
   }
 
   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",
   "description": "",
   "main": "src/index.ts",
-  "scripts": {},
+  "scripts": {
+    "lint": "eslint --ext .ts src",
+    "tsc": "tsc --noEmit"
+  },
   "keywords": [],
   "author": "",
   "license": "ISC",

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

@@ -23,10 +23,16 @@ const repoCommandSchema = z.object({
 
 const systemCommandSchema = z.object({
   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({
   success: z.boolean(),

+ 0 - 1
packages/shared/tsconfig.json

@@ -19,7 +19,6 @@
     "resolveJsonModule": true,
     "isolatedModules": true,
     "jsx": "preserve",
-    "incremental": true,
     "strictNullChecks": true,
     "allowSyntheticDefaultImports": 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 { getAppEnvMap } from '@/server/utils/env-generation';
 import { appInfoSchema } from '@runtipi/shared';
 import { fileExists, readdirSync, readFile, readJsonFile } from '../../common/fs.helpers';
 import { getConfig } from '../../core/TipiConfig';
@@ -32,44 +30,6 @@ export const checkAppRequirements = (appName: string) => {
   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,
   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');
 
       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;
 
-        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);
       }
 
-      return { current: TipiConfig.getConfig().version, latest: version?.replace('v', ''), body };
+      return { current: TipiConfig.getConfig().version, latest: version, body };
     } catch (e) {
       Logger.error(e);
       return { current: TipiConfig.getConfig().version, latest: undefined };
@@ -101,7 +101,7 @@ export class SystemServiceClass {
 
     TipiConfig.setConfig('status', 'UPDATING');
 
-    this.dispatcher.dispatchEvent({ type: 'system', command: 'update' });
+    this.dispatcher.dispatchEvent({ type: 'system', command: 'update', version: latest });
 
     return true;
   };