feat: create app update event
This commit is contained in:
parent
77dd73bdb4
commit
56c9d51d13
14 changed files with 83 additions and 94 deletions
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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/**/*",
|
||||
|
|
|
@ -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';
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
packages/shared/.eslintignore
Normal file
1
packages/shared/.eslintignore
Normal file
|
@ -0,0 +1 @@
|
|||
.eslintrc.js
|
39
packages/shared/.eslintrc.js
Normal file
39
packages/shared/.eslintrc.js
Normal file
|
@ -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,
|
||||
},
|
||||
};
|
|
@ -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",
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"strictNullChecks": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue