Compare commits
No commits in common. "develop" and "v0.7.2" have entirely different histories.
677 changed files with 21040 additions and 35865 deletions
|
@ -36,11 +36,7 @@
|
|||
"contributions": [
|
||||
"code",
|
||||
"ideas",
|
||||
"test",
|
||||
"content",
|
||||
"promotion",
|
||||
"question",
|
||||
"review"
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -150,264 +146,13 @@
|
|||
"contributions": [
|
||||
"content"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "fsackur",
|
||||
"name": "Freddie Sackur",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/3678789?v=4",
|
||||
"profile": "https://fsackur.github.io/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "innocentius",
|
||||
"name": "Innocentius",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5344432?v=4",
|
||||
"profile": "http://innocentius.github.io",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "TetrisIQ",
|
||||
"name": "Alex",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/24246993?v=4",
|
||||
"profile": "https://github.com/TetrisIQ",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ruibaby",
|
||||
"name": "Ryan Wang",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/21301288?v=4",
|
||||
"profile": "https://ryanc.cc",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "simonandr",
|
||||
"name": "simonandr",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/48092304?v=4",
|
||||
"profile": "https://github.com/simonandr",
|
||||
"contributions": [
|
||||
"content"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "demizeu",
|
||||
"name": "iepure",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/121183951?v=4",
|
||||
"profile": "https://github.com/demizeu",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "SergeyKodolov",
|
||||
"name": "Sergey Kodolov",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/35339452?v=4",
|
||||
"profile": "https://github.com/SergeyKodolov",
|
||||
"contributions": [
|
||||
"translation",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sclaren",
|
||||
"name": "sclaren",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/915292?v=4",
|
||||
"profile": "https://github.com/sclaren",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mcmeel",
|
||||
"name": "mcmeel",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/13773536?v=4",
|
||||
"profile": "https://github.com/mcmeel",
|
||||
"contributions": [
|
||||
"question",
|
||||
"ideas",
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "NoisyFridge",
|
||||
"name": "NoisyFridge",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/73795785?v=4",
|
||||
"profile": "https://github.com/NoisyFridge",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Bvoxl",
|
||||
"name": "Bvoxl",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/67489519?v=4",
|
||||
"profile": "https://github.com/Bvoxl",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "m-lab-0",
|
||||
"name": "m-lab-0",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/116570617?v=4",
|
||||
"profile": "https://github.com/m-lab-0",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dannkunt",
|
||||
"name": "dannkunt",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/32395839?v=4",
|
||||
"profile": "https://github.com/dannkunt",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Schmanko",
|
||||
"name": "Schmanko",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/94195393?v=4",
|
||||
"profile": "https://github.com/Schmanko",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "nghialele",
|
||||
"name": "Nghia Lele",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/129353223?v=4",
|
||||
"profile": "https://micro.nghialele.com",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "amusingimpala75",
|
||||
"name": "amusingimpala75",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/69653100?v=4",
|
||||
"profile": "https://github.com/amusingimpala75",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "M1n-4d316e",
|
||||
"name": "David",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/54779580?v=4",
|
||||
"profile": "http://m1n.omg.lol",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "steveiliop56",
|
||||
"name": "Stavros Iliopoulos",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/106091011?v=4",
|
||||
"profile": "https://github.com/steveiliop56",
|
||||
"contributions": [
|
||||
"translation",
|
||||
"code",
|
||||
"test"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "loxiry",
|
||||
"name": "loxiry",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/86959495?v=4",
|
||||
"profile": "https://github.com/loxiry",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "JigSawFr",
|
||||
"name": "JigSaw",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5781907?v=4",
|
||||
"profile": "https://github.com/JigSawFr",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "DireMunchkin",
|
||||
"name": "DireMunchkin",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1665676?v=4",
|
||||
"profile": "https://github.com/DireMunchkin",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "FabioCingottini",
|
||||
"name": "Fabio Cingottini",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/32102735?v=4",
|
||||
"profile": "https://github.com/FabioCingottini",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "itsrllyhim",
|
||||
"name": "him",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/143047010?v=4",
|
||||
"profile": "https://github.com/itsrllyhim",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "cchalop1",
|
||||
"name": "CHALOPIN Clément",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/28163855?v=4",
|
||||
"profile": "http://cchalop1.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "geetansh",
|
||||
"name": "Geetansh Jindal",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/9976198?v=4",
|
||||
"profile": "https://github.com/geetansh",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "0livier",
|
||||
"name": "Olivier Garcia",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/10607?v=4",
|
||||
"profile": "https://github.com/0livier",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "qcoudeyr",
|
||||
"name": "qcoudeyr",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/124463277?v=4",
|
||||
"profile": "https://github.com/qcoudeyr",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
"projectName": "runtipi",
|
||||
"projectOwner": "runtipi",
|
||||
"projectOwner": "meienberger",
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"skipCi": true,
|
||||
"commitConvention": "angular",
|
||||
"commitType": "docs"
|
||||
"commitConvention": "angular"
|
||||
}
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"image": "mcr.microsoft.com/vscode/devcontainers/javascript-node",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/docker-in-docker": {
|
||||
"version": "latest",
|
||||
"moby": true
|
||||
}
|
||||
},
|
||||
"extensions": [
|
||||
"ms-azuretools.vscode-docker",
|
||||
"ms-vscode.vscode-typescript-next",
|
||||
"waderyan.gitblame"
|
||||
],
|
||||
"postCreateCommand": "./.devcontainer/postCreateCommand.sh"
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# We need to change the owner of the files in the app-data folder
|
||||
# if this failes we have to change the permission your self
|
||||
fswatch --event=Created /workspaces/runtipi/app-data/ | \
|
||||
xargs -l1 sh -c 'echo "$1" && sudo chown node "$1" -R' -- &
|
|
@ -1,12 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
echo '{
|
||||
"appsRepoUrl": "https://github.com/runtipi/runtipi-appstore.git/"
|
||||
}' > state/settings.json
|
||||
npm i -g pnpm
|
||||
pnpm i
|
||||
sudo apt-get update
|
||||
sudo apt-get install jq fswatch -y
|
||||
mkdir logs
|
||||
mkdir data
|
||||
sudo chown node logs
|
||||
sudo chown node data
|
|
@ -10,17 +10,3 @@ dist/
|
|||
docker-compose*.yml
|
||||
Dockerfile*
|
||||
.dockerignore
|
||||
|
||||
# Tipi folder
|
||||
logs/
|
||||
state/
|
||||
templates/
|
||||
scripts/
|
||||
screenshots/
|
||||
repos/
|
||||
media/
|
||||
data/
|
||||
apps/
|
||||
app-data/
|
||||
.github/
|
||||
__mocks__/
|
||||
|
|
22
.env.example
22
.env.example
|
@ -1,22 +0,0 @@
|
|||
APPS_REPO_ID=7a92c8307e0a8074763c80be1fcfa4f87da6641daea9211aea6743b0116aba3b
|
||||
APPS_REPO_URL=https://github.com/runtipi/runtipi-appstore
|
||||
TZ=Etc/UTC
|
||||
INTERNAL_IP=localhost
|
||||
DNS_IP=9.9.9.9
|
||||
ARCHITECTURE=arm64
|
||||
TIPI_VERSION=1.5.2
|
||||
JWT_SECRET=secret
|
||||
ROOT_FOLDER_HOST=/path/to/runtipi
|
||||
STORAGE_PATH=/path/to/runtipi
|
||||
NGINX_PORT=7000
|
||||
NGINX_PORT_SSL=443
|
||||
DOMAIN=tipi.localhost
|
||||
POSTGRES_HOST=tipi-db
|
||||
POSTGRES_DBNAME=tipi
|
||||
POSTGRES_USERNAME=tipi
|
||||
POSTGRES_PASSWORD=postgres
|
||||
POSTGRES_PORT=5432
|
||||
REDIS_HOST=tipi-redis
|
||||
REDIS_PASSWORD=redis
|
||||
DEMO_MODE=false
|
||||
LOCAL_DOMAIN=tipi.lan
|
14
.env.test
14
.env.test
|
@ -1,14 +0,0 @@
|
|||
POSTGRES_HOST=localhost
|
||||
POSTGRES_DBNAME=postgres
|
||||
POSTGRES_USERNAME=postgres
|
||||
POSTGRES_PASSWORD=postgres
|
||||
POSTGRES_PORT=5433
|
||||
APPS_REPO_ID=repo-id
|
||||
APPS_REPO_URL=https://test.com/test
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PASSWORD=redis
|
||||
INTERNAL_IP=localhost
|
||||
TIPI_VERSION=1
|
||||
JWT_SECRET=secret
|
||||
DOMAIN=tipi.localhost
|
||||
LOCAL_DOMAIN=tipi.lan
|
80
.eslintrc.js
80
.eslintrc.js
|
@ -1,80 +0,0 @@
|
|||
module.exports = {
|
||||
plugins: ['@typescript-eslint', 'import', 'react', 'jest', 'jsx-a11y', 'testing-library', 'jest-dom'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'next/core-web-vitals',
|
||||
'next',
|
||||
'airbnb',
|
||||
'airbnb-typescript',
|
||||
'eslint:recommended',
|
||||
'plugin:import/typescript',
|
||||
'prettier',
|
||||
'plugin:react/recommended',
|
||||
'plugin:jsx-a11y/recommended',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: './tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
rules: {
|
||||
'no-restricted-exports': 0,
|
||||
'no-redeclare': 0, // already handled by @typescript-eslint/no-redeclare
|
||||
'react/display-name': 0,
|
||||
'react/prop-types': 0,
|
||||
'react/function-component-definition': 0,
|
||||
'react/require-default-props': 0,
|
||||
'import/prefer-default-export': 0,
|
||||
'react/jsx-props-no-spreading': 0,
|
||||
'react/no-unused-prop-types': 0,
|
||||
'react/button-has-type': 0,
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
{
|
||||
devDependencies: [
|
||||
'esbuild.js',
|
||||
'e2e/**',
|
||||
'**/*.test.{ts,tsx}',
|
||||
'**/*.spec.{ts,tsx}',
|
||||
'**/*.factory.{ts,tsx}',
|
||||
'**/mocks/**',
|
||||
'**/__mocks__/**',
|
||||
'tests/**',
|
||||
'**/*.d.ts',
|
||||
'**/*.workspace.ts',
|
||||
'**/*.setup.{ts,js}',
|
||||
'**/*.config.{ts,js}',
|
||||
],
|
||||
},
|
||||
],
|
||||
'no-underscore-dangle': 0,
|
||||
'arrow-body-style': 0,
|
||||
'class-methods-use-this': 0,
|
||||
'import/extensions': [
|
||||
'error',
|
||||
'ignorePackages',
|
||||
{
|
||||
'': 'never',
|
||||
js: 'never',
|
||||
jsx: 'never',
|
||||
ts: 'never',
|
||||
tsx: 'never',
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.test.ts', '*.test.tsx'],
|
||||
extends: ['plugin:jest-dom/recommended', 'plugin:testing-library/react'],
|
||||
},
|
||||
],
|
||||
globals: {
|
||||
JSX: true,
|
||||
NodeJS: true,
|
||||
},
|
||||
env: {
|
||||
'jest/globals': true,
|
||||
},
|
||||
};
|
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -11,7 +11,7 @@ assignees: meienberger
|
|||
Before opening your issue be sure to have completed all those tasks.
|
||||
- [ ] I have searched for an already existing issue with similar context and errors. My issue has not yet been reported.
|
||||
- [ ] I have included a clear description and steps to reproduce.
|
||||
- [ ] I have included logs from the file `runtipi/logs/error.log` if relevant
|
||||
- [ ] I have included my OS information
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
@ -29,10 +29,11 @@ A clear and concise description of what you expected to happen.
|
|||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Server (please complete the following information):**
|
||||
- OS: [e.g. Ubuntu 20.04]
|
||||
- Tipi Version [e.g. 2.0.5] (can be found in settings page)
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Please include logs here `runtipi/logs/error.log` and add any other context about the problem here. Like results of the `start` script or container logs `docker logs ...`
|
||||
Add any other context about the problem here. Like results of the `start` script or container logs
|
||||
|
||||
|
|
20
.github/dependabot.yml
vendored
20
.github/dependabot.yml
vendored
|
@ -1,20 +0,0 @@
|
|||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: 'npm'
|
||||
directory: '/'
|
||||
versioning-strategy: increase
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
open-pull-requests-limit: 10
|
||||
rebase-strategy: 'auto'
|
||||
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
rebase-strategy: 'auto'
|
17
.github/stale.yml
vendored
17
.github/stale.yml
vendored
|
@ -1,17 +0,0 @@
|
|||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 30
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions. If you feel this issue should remain open, please
|
||||
add a comment with the requested information and we will keep it open.
|
||||
closeComment: false
|
165
.github/workflows/alpha-release.yml
vendored
165
.github/workflows/alpha-release.yml
vendored
|
@ -1,165 +0,0 @@
|
|||
name: Alpha Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Alpha version tag (1, 2, 3, ...)'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
create-tag:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tagname: ${{ steps.get_tag.outputs.tagname }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get tag from package.json
|
||||
id: get_tag
|
||||
run: |
|
||||
VERSION=$(npm run version --silent)
|
||||
echo "tagname=v${VERSION}-alpha.${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: rickstaa/action-create-tag@v1
|
||||
with:
|
||||
tag: ${{ steps.get_tag.outputs.tagname }}
|
||||
|
||||
build-worker:
|
||||
runs-on: ubuntu-latest
|
||||
needs: create-tag
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push images
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./packages/worker/Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ghcr.io/${{ github.repository_owner }}/worker:${{ needs.create-tag.outputs.tagname }}
|
||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/worker:buildcache
|
||||
cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/worker:buildcache,mode=max
|
||||
|
||||
build-images:
|
||||
runs-on: ubuntu-latest
|
||||
needs: create-tag
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push images
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ghcr.io/${{ github.repository_owner }}/runtipi:${{ needs.create-tag.outputs.tagname }}
|
||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/runtipi:buildcache
|
||||
cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/runtipi:buildcache,mode=max
|
||||
|
||||
build-cli:
|
||||
runs-on: ubuntu-latest
|
||||
needs: create-tag
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v2.4.0
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Set version
|
||||
run: pnpm -r --filter cli set-version ${{ needs.create-tag.outputs.tagname }}
|
||||
|
||||
- name: Build CLI
|
||||
run: pnpm -r --filter cli package
|
||||
|
||||
- name: Upload CLI
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: cli
|
||||
path: packages/cli/dist
|
||||
|
||||
publish-release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [create-tag, build-images, build-cli, build-worker]
|
||||
|
||||
steps:
|
||||
- name: Download CLI
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: cli
|
||||
path: cli
|
||||
|
||||
- name: Rename CLI
|
||||
run: |
|
||||
mv cli/bin/cli-x64 ./runtipi-cli-linux-x64
|
||||
|
||||
- name: Create alpha release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
body: |
|
||||
**${{ needs.create-tag.outputs.tagname }}**
|
||||
tag_name: ${{ needs.create-tag.outputs.tagname }}
|
||||
name: ${{ needs.create-tag.outputs.tagname }}
|
||||
draft: false
|
||||
prerelease: true
|
||||
files: |
|
||||
runtipi-cli-linux-x64
|
175
.github/workflows/beta-release.yml
vendored
175
.github/workflows/beta-release.yml
vendored
|
@ -1,175 +0,0 @@
|
|||
name: Beta Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Beta version tag (1, 2, 3, ...)'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
create-tag:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tagname: ${{ steps.get_tag.outputs.tagname }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get tag from package.json
|
||||
id: get_tag
|
||||
run: |
|
||||
VERSION=$(npm run version --silent)
|
||||
echo "tagname=v${VERSION}-beta.${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: rickstaa/action-create-tag@v1
|
||||
with:
|
||||
tag: ${{ steps.get_tag.outputs.tagname }}
|
||||
|
||||
build-worker:
|
||||
runs-on: ubuntu-latest
|
||||
needs: create-tag
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push images
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./packages/worker/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ghcr.io/${{ github.repository_owner }}/worker:${{ needs.create-tag.outputs.tagname }}
|
||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/worker:buildcache
|
||||
cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/worker:buildcache,mode=max
|
||||
|
||||
build-images:
|
||||
needs: create-tag
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push images
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ghcr.io/${{ github.repository_owner }}/runtipi:${{ needs.create-tag.outputs.tagname }}
|
||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/runtipi:buildcache
|
||||
cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/runtipi:buildcache,mode=max
|
||||
|
||||
build-cli:
|
||||
runs-on: ubuntu-latest
|
||||
needs: create-tag
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v2.4.0
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Set version
|
||||
run: pnpm -r --filter cli set-version ${{ needs.create-tag.outputs.tagname }}
|
||||
|
||||
- name: Build CLI
|
||||
run: pnpm -r --filter cli package
|
||||
|
||||
- name: Upload CLI
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: cli
|
||||
path: packages/cli/dist
|
||||
|
||||
publish-release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [create-tag, build-images, build-cli, build-worker]
|
||||
outputs:
|
||||
id: ${{ steps.create_release.outputs.id }}
|
||||
steps:
|
||||
- name: Download CLI
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: cli
|
||||
path: cli
|
||||
|
||||
- name: Rename CLI
|
||||
run: |
|
||||
mv cli/bin/cli-x64 ./runtipi-cli-linux-x64
|
||||
mv cli/bin/cli-arm64 ./runtipi-cli-linux-arm64
|
||||
|
||||
- name: Create beta release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
body: |
|
||||
**${{ needs.create-tag.outputs.tagname }}**
|
||||
tag_name: ${{ needs.create-tag.outputs.tagname }}
|
||||
name: ${{ needs.create-tag.outputs.tagname }}
|
||||
draft: false
|
||||
prerelease: true
|
||||
files: |
|
||||
runtipi-cli-linux-x64
|
||||
runtipi-cli-linux-arm64
|
||||
|
||||
e2e-tests:
|
||||
needs: [create-tag, publish-release]
|
||||
uses: './.github/workflows/e2e.yml'
|
||||
secrets: inherit
|
||||
with:
|
||||
version: ${{ needs.create-tag.outputs.tagname }}
|
108
.github/workflows/ci.yml
vendored
108
.github/workflows/ci.yml
vendored
|
@ -1,27 +1,16 @@
|
|||
name: Tipi CI
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
|
||||
env:
|
||||
ROOT_FOLDER: /runtipi
|
||||
JWT_SECRET: 'secret'
|
||||
ROOT_FOLDER_HOST: /runtipi
|
||||
JWT_SECRET: "secret"
|
||||
ROOT_FOLDER_HOST: /tipi
|
||||
APPS_REPO_ID: repo-id
|
||||
INTERNAL_IP: localhost
|
||||
REDIS_HOST: redis
|
||||
REDIS_PASSWORD: redis
|
||||
APPS_REPO_URL: https://repo.github.com/
|
||||
DOMAIN: localhost
|
||||
LOCAL_DOMAIN: tipi.lan
|
||||
TIPI_VERSION: 0.0.1
|
||||
POSTGRES_HOST: localhost
|
||||
POSTGRES_DBNAME: postgres
|
||||
POSTGRES_USERNAME: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_PORT: 5433
|
||||
|
||||
INTERNAL_IP: 192.168.1.10
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
|
@ -38,24 +27,24 @@ jobs:
|
|||
--health-retries 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 16
|
||||
|
||||
- uses: pnpm/action-setup@v2.4.0
|
||||
- uses: pnpm/action-setup@v2.2.2
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 8
|
||||
version: 7
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
|
@ -68,74 +57,15 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages
|
||||
run: pnpm -r build
|
||||
|
||||
- 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@v2
|
||||
|
||||
run: pnpm -r lint
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm run test --max-workers ${{ steps.cpu-cores.outputs.count }}
|
||||
run: pnpm -r test
|
||||
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./coverage/lcov.info
|
||||
flags: app
|
||||
|
||||
- name: Run packages tests
|
||||
run: pnpm -r test
|
||||
|
||||
- name: Upload CLI coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/cli/coverage/lcov.info
|
||||
flags: cli
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v2.4.0
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build client
|
||||
run: npm run build
|
||||
|
||||
- name: Run tsc
|
||||
run: pnpm run tsc
|
||||
|
||||
- name: Run packages tsc
|
||||
run: pnpm -r run tsc
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
4
.github/workflows/dependency-review.yml
vendored
4
.github/workflows/dependency-review.yml
vendored
|
@ -15,6 +15,6 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v3
|
||||
uses: actions/dependency-review-action@v1
|
||||
|
|
203
.github/workflows/e2e.yml
vendored
203
.github/workflows/e2e.yml
vendored
|
@ -1,203 +0,0 @@
|
|||
name: E2E Tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
description: 'Version to test (e.g. v1.6.0-beta.1)'
|
||||
outputs:
|
||||
page_url:
|
||||
description: 'URL of the deployed report'
|
||||
value: ${{ jobs.report-deployment.outputs.page_url }}
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
description: 'Version to test (e.g. v1.6.0-beta.1)'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
droplet_id: ${{ steps.create-droplet.outputs.droplet_id }}
|
||||
droplet_ip: ${{ steps.get-droplet-ip.outputs.droplet_ip }}
|
||||
postgres_password: ${{ steps.get-postgres-password.outputs.postgres_password }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install SSH key
|
||||
uses: shimataro/ssh-key-action@v2
|
||||
with:
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
known_hosts: unnecessary
|
||||
name: id_rsa
|
||||
|
||||
- name: Get sha of last commit
|
||||
id: get-sha
|
||||
run: echo "sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install doctl
|
||||
uses: digitalocean/action-doctl@v2
|
||||
with:
|
||||
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
|
||||
|
||||
- name: Create new Droplet
|
||||
id: create-droplet
|
||||
run: |
|
||||
droplet_id=$(doctl compute droplet create runtipi-${{ steps.get-sha.outputs.sha }} \
|
||||
--image ubuntu-20-04-x64 \
|
||||
--size s-1vcpu-1gb \
|
||||
--format ID \
|
||||
--no-header \
|
||||
--ssh-keys ${{ secrets.SSH_KEY_FINGERPRINT }})
|
||||
echo "droplet_id=$droplet_id" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Wait for Droplet to become active
|
||||
run: |
|
||||
while ! doctl compute droplet get ${{ steps.create-droplet.outputs.droplet_id }} --format Status --no-header | grep -q "active"; do sleep 5; done
|
||||
|
||||
- name: Get Droplet IP address
|
||||
id: get-droplet-ip
|
||||
run: |
|
||||
droplet_ip=$(doctl compute droplet get ${{ steps.create-droplet.outputs.droplet_id }} --format PublicIPv4 --no-header)
|
||||
echo "droplet_ip=$droplet_ip" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Wait for SSH to be ready on Droplet
|
||||
run: |
|
||||
while ! ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa root@${{ steps.get-droplet-ip.outputs.droplet_ip }} "echo 'SSH is ready'"; do sleep 5; done
|
||||
|
||||
- name: Create docker group on Droplet
|
||||
uses: fifsky/ssh-action@master
|
||||
with:
|
||||
command: |
|
||||
groupadd docker
|
||||
usermod -aG docker root
|
||||
host: ${{ steps.get-droplet-ip.outputs.droplet_ip }}
|
||||
user: root
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
|
||||
- name: Wait 90 seconds for Docker to be ready on Droplet
|
||||
run: sleep 90
|
||||
|
||||
- name: Deploy app to Droplet
|
||||
uses: fifsky/ssh-action@master
|
||||
with:
|
||||
command: |
|
||||
echo 'Downloading install script from GitHub'
|
||||
curl -s https://raw.githubusercontent.com/runtipi/runtipi/${{ inputs.version }}/scripts/install.sh > install.sh
|
||||
chmod +x install.sh
|
||||
echo 'Running install script'
|
||||
./install.sh --version ${{ inputs.version }}
|
||||
echo 'App deployed'
|
||||
host: ${{ steps.get-droplet-ip.outputs.droplet_ip }}
|
||||
user: root # TODO: use non-root user
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
|
||||
- name: Get POSTGRES_PASSWORD from .env file
|
||||
id: get-postgres-password
|
||||
run: |
|
||||
postgres_password=$(ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa root@${{ steps.get-droplet-ip.outputs.droplet_ip }} "cat ./runtipi/.env | grep POSTGRES_PASSWORD | cut -d '=' -f2")
|
||||
echo "postgres_password=$postgres_password" >> $GITHUB_OUTPUT
|
||||
|
||||
e2e:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
needs: [deploy]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v2.4.0
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Create .env.e2e file with Droplet IP
|
||||
run: |
|
||||
echo "SERVER_IP=${{ needs.deploy.outputs.droplet_ip }}" > .env.e2e
|
||||
echo "POSTGRES_PASSWORD=${{ needs.deploy.outputs.postgres_password }}" >> .env.e2e
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
- name: Run Playwright tests
|
||||
id: run-e2e
|
||||
run: npm run test:e2e
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 7
|
||||
|
||||
report-deployment:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [e2e]
|
||||
outputs:
|
||||
page_url: ${{ steps.deployment.outputs.page_url }}
|
||||
permissions:
|
||||
pages: write # to deploy to Pages
|
||||
id-token: write # to verify the deployment originates from an appropriate source
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
if: always()
|
||||
steps:
|
||||
- name: Download report artifact
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v3
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v2
|
||||
with:
|
||||
path: playwright-report/
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v2
|
||||
|
||||
teardown:
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
needs: [e2e, deploy]
|
||||
steps:
|
||||
- name: Install doctl
|
||||
uses: digitalocean/action-doctl@v2
|
||||
with:
|
||||
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
|
||||
|
||||
- name: Delete Droplet
|
||||
run: doctl compute droplet delete ${{ needs.deploy.outputs.droplet_id }} --force
|
43
.github/workflows/release-candidate.yml
vendored
Normal file
43
.github/workflows/release-candidate.yml
vendored
Normal file
|
@ -0,0 +1,43 @@
|
|||
name: Release candidate
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- release/*
|
||||
|
||||
jobs:
|
||||
# Build images and publish RCs to DockerHub
|
||||
build-images:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get tag from VERSION file
|
||||
id: meta
|
||||
run: |
|
||||
VERSION=$(npm run version --silent)
|
||||
TAG=${VERSION}
|
||||
echo "::set-output name=tag::${TAG}"
|
||||
|
||||
- name: Build and push images
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
||||
tags: meienberger/runtipi:rc-${{ steps.meta.outputs.TAG }}
|
||||
cache-from: type=registry,ref=meienberger/runtipi:buildcache
|
||||
cache-to: type=registry,ref=meienberger/runtipi:buildcache,mode=max
|
210
.github/workflows/release.yml
vendored
210
.github/workflows/release.yml
vendored
|
@ -1,192 +1,62 @@
|
|||
|
||||
name: Publish release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
create-tag:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-images, build-cli]
|
||||
outputs:
|
||||
tagname: ${{ steps.get_tag.outputs.tagname }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get tag from package.json
|
||||
id: get_tag
|
||||
run: |
|
||||
VERSION=$(npm run version --silent)
|
||||
echo "tagname=v${VERSION}" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: rickstaa/action-create-tag@v1
|
||||
with:
|
||||
tag: ${{ steps.get_tag.outputs.tagname }}
|
||||
|
||||
build-images:
|
||||
if: github.repository == 'runtipi/runtipi'
|
||||
needs: create-tag
|
||||
release:
|
||||
if: github.repository == 'meienberger/runtipi'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Get tag from VERSION file
|
||||
id: meta
|
||||
run: |
|
||||
VERSION=$(npm run version --silent)
|
||||
TAG=${VERSION}
|
||||
echo "::set-output name=tag::${TAG}"
|
||||
|
||||
- name: Build and push images
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
||||
tags: ghcr.io/${{ github.repository_owner }}/runtipi:${{ needs.create-tag.outputs.tagname }},ghcr.io/${{ github.repository_owner }}/runtipi:latest
|
||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/runtipi:buildcache
|
||||
cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/runtipi:buildcache,mode=max
|
||||
tags: meienberger/runtipi:latest,meienberger/runtipi:${{ steps.meta.outputs.TAG }}
|
||||
cache-from: type=registry,ref=meienberger/runtipi:buildcache
|
||||
cache-to: type=registry,ref=meienberger/runtipi:buildcache,mode=max
|
||||
|
||||
build-worker:
|
||||
runs-on: ubuntu-latest
|
||||
needs: create-tag
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
- name: Create Tag
|
||||
id: create_tag
|
||||
uses: jaywcjlove/create-tag-action@v1.1.5
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push images
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./packages/worker/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ghcr.io/${{ github.repository_owner }}/worker:${{ needs.create-tag.outputs.tagname }},ghcr.io/${{ github.repository_owner }}/worker:latest
|
||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/worker:buildcache
|
||||
cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/worker:buildcache,mode=max
|
||||
|
||||
build-cli:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
needs: create-tag
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v2.4.0
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
run: |
|
||||
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Set version
|
||||
run: pnpm -r --filter cli set-version ${{ needs.create-tag.outputs.tagname }}
|
||||
|
||||
- name: Build CLI
|
||||
run: pnpm -r --filter cli package
|
||||
|
||||
- name: Upload CLI
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: cli
|
||||
path: packages/cli/dist
|
||||
|
||||
publish-release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [create-tag, build-images, build-worker, build-cli]
|
||||
outputs:
|
||||
id: ${{ steps.create_release.outputs.id }}
|
||||
steps:
|
||||
- name: Download CLI
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: cli
|
||||
path: cli
|
||||
|
||||
- name: Rename CLI
|
||||
run: |
|
||||
mv cli/bin/cli-x64 ./runtipi-cli-linux-x64
|
||||
mv cli/bin/cli-arm64 ./runtipi-cli-linux-arm64
|
||||
|
||||
- name: Create release
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
package-path: ./package.json
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: actions/create-release@latest
|
||||
if: steps.create_tag.outputs.successful
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
body: |
|
||||
**${{ needs.create-tag.outputs.tagname }}**
|
||||
tag_name: ${{ needs.create-tag.outputs.tagname }}
|
||||
name: ${{ needs.create-tag.outputs.tagname }}
|
||||
tag_name: ${{ steps.create_tag.outputs.version }}
|
||||
release_name: ${{ steps.create_tag.outputs.version }}
|
||||
draft: false
|
||||
prerelease: true
|
||||
files: |
|
||||
runtipi-cli-linux-x64
|
||||
runtipi-cli-linux-arm64
|
||||
|
||||
e2e-tests:
|
||||
needs: [create-tag, publish-release]
|
||||
uses: './.github/workflows/e2e.yml'
|
||||
secrets: inherit
|
||||
with:
|
||||
version: ${{ needs.create-tag.outputs.tagname }}
|
||||
|
||||
# Promote release if e2e tests succeed
|
||||
promote:
|
||||
needs: [publish-release, e2e-tests]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Promote release
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const id = '${{ needs.publish-release.outputs.id }}';
|
||||
github.rest.repos.updateRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: id,
|
||||
draft: false,
|
||||
prerelease: false
|
||||
});
|
||||
prerelease: false
|
||||
|
|
80
.gitignore
vendored
80
.gitignore
vendored
|
@ -3,67 +3,39 @@
|
|||
|
||||
.DS_Store
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
/dist
|
||||
server-preload.js
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
logs
|
||||
.pnpm-debug.log
|
||||
.env*
|
||||
!.env.example
|
||||
!.env.test
|
||||
github.secrets
|
||||
node_modules/
|
||||
/app-data/
|
||||
/data/
|
||||
/repos/
|
||||
/apps/
|
||||
/traefik/
|
||||
app-data/*
|
||||
data/postgres
|
||||
data/redis
|
||||
!app-data/.gitkeep
|
||||
repos/*
|
||||
!repos/.gitkeep
|
||||
apps/*
|
||||
!apps/.gitkeep
|
||||
traefik/shared
|
||||
|
||||
# media folder
|
||||
media
|
||||
!media/.gitkeep
|
||||
!media/data/.gitkeep
|
||||
!media/data/books/.gitkeep
|
||||
!media/data/books/ebooks/.gitkeep
|
||||
!media/data/books/spoken/.gitkeep
|
||||
!media/data/movies/.gitkeep
|
||||
!media/data/music/.gitkeep
|
||||
!media/data/podcasts/.gitkeep
|
||||
!media/data/tv/.gitkeep
|
||||
!media/data/images/.gitkeep
|
||||
!media/torrents/.gitkeep
|
||||
!media/torrents/complete/.gitkeep
|
||||
!media/torrents/incomplete/.gitkeep
|
||||
!media/torrents/watch/.gitkeep
|
||||
|
||||
/state/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
temp
|
||||
|
||||
./traefik/
|
||||
/user-config/
|
||||
# state folder
|
||||
state/*
|
||||
!state/.gitkeep
|
||||
|
|
4
.husky/commit-msg
Executable file
4
.husky/commit-msg
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx --no -- commitlint --edit $1
|
2
.husky/pre-commit
Executable file
2
.husky/pre-commit
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
10
.husky/pre-push
Executable file
10
.husky/pre-push
Executable file
|
@ -0,0 +1,10 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
# If test-db is not running with docker
|
||||
if ! docker ps | grep -q test-db; then
|
||||
npm run start:pg
|
||||
fi
|
||||
|
||||
pnpm -r test
|
||||
pnpm -r lint:fix
|
74
Dockerfile
74
Dockerfile
|
@ -1,49 +1,47 @@
|
|||
ARG NODE_VERSION="20.10"
|
||||
ARG ALPINE_VERSION="3.18"
|
||||
FROM node:18 AS builder
|
||||
|
||||
FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS node_base
|
||||
RUN npm install node-gyp -g
|
||||
|
||||
FROM node_base AS builder_base
|
||||
|
||||
RUN npm install pnpm -g
|
||||
|
||||
# BUILDER
|
||||
FROM builder_base AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./pnpm-lock.yaml ./
|
||||
COPY ./pnpm-workspace.yaml ./
|
||||
COPY ./patches ./patches
|
||||
RUN pnpm fetch --no-scripts
|
||||
|
||||
COPY ./package*.json ./
|
||||
COPY ./packages/shared ./packages/shared
|
||||
|
||||
RUN pnpm install -r --prefer-offline
|
||||
COPY ./src ./src
|
||||
COPY ./tsconfig.json ./tsconfig.json
|
||||
COPY ./next.config.mjs ./next.config.mjs
|
||||
COPY ./public ./public
|
||||
COPY ./tests ./tests
|
||||
WORKDIR /api
|
||||
COPY ./packages/system-api/package.json /api/package.json
|
||||
RUN npm i
|
||||
# ---
|
||||
WORKDIR /dashboard
|
||||
COPY ./packages/dashboard/package.json /dashboard/package.json
|
||||
RUN npm i
|
||||
|
||||
WORKDIR /api
|
||||
COPY ./packages/system-api /api
|
||||
RUN npm run build
|
||||
# ---
|
||||
WORKDIR /dashboard
|
||||
COPY ./packages/dashboard /dashboard
|
||||
RUN npm run build
|
||||
|
||||
# APP
|
||||
FROM node_base AS app
|
||||
|
||||
ENV NODE_ENV production
|
||||
FROM alpine:3.16.0 as app
|
||||
|
||||
USER node
|
||||
WORKDIR /
|
||||
|
||||
WORKDIR /app
|
||||
# # Install dependencies
|
||||
RUN apk --no-cache add nodejs npm
|
||||
RUN apk --no-cache add g++
|
||||
RUN apk --no-cache add make
|
||||
RUN apk --no-cache add python3
|
||||
|
||||
COPY --from=builder /app/next.config.mjs ./
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
COPY --from=builder --chown=node:node /app/.next/standalone ./
|
||||
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
|
||||
RUN npm install node-gyp -g
|
||||
|
||||
EXPOSE 3000
|
||||
WORKDIR /api
|
||||
COPY ./packages/system-api/package*.json /api/
|
||||
RUN npm install --omit=dev
|
||||
|
||||
CMD ["npm", "run", "start"]
|
||||
COPY --from=builder /api/dist /api/dist
|
||||
|
||||
WORKDIR /dashboard
|
||||
COPY --from=builder /dashboard/next.config.js ./
|
||||
COPY --from=builder /dashboard/public ./public
|
||||
COPY --from=builder /dashboard/package.json ./package.json
|
||||
COPY --from=builder --chown=node:node /dashboard/.next/standalone ./
|
||||
COPY --from=builder --chown=node:node /dashboard/.next/static ./.next/static
|
||||
|
||||
WORKDIR /
|
|
@ -1,23 +1,19 @@
|
|||
ARG NODE_VERSION="20.10"
|
||||
ARG ALPINE_VERSION="3.18"
|
||||
FROM node:18-alpine3.16
|
||||
|
||||
FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION}
|
||||
WORKDIR /
|
||||
|
||||
RUN npm install pnpm -g
|
||||
RUN apk --no-cache add g++ make
|
||||
RUN npm install node-gyp -g
|
||||
|
||||
WORKDIR /app
|
||||
WORKDIR /api
|
||||
COPY ./packages/system-api/package*.json /api/
|
||||
RUN npm install
|
||||
|
||||
COPY ./pnpm-lock.yaml ./
|
||||
COPY ./patches ./patches
|
||||
RUN pnpm fetch --ignore-scripts
|
||||
WORKDIR /dashboard
|
||||
COPY ./packages/dashboard/package*.json /dashboard/
|
||||
RUN npm install
|
||||
|
||||
COPY ./package*.json ./
|
||||
COPY ./packages/shared ./packages/shared
|
||||
COPY ./packages/system-api /api
|
||||
COPY ./packages/dashboard /dashboard
|
||||
|
||||
RUN pnpm install -r --prefer-offline
|
||||
|
||||
COPY ./tsconfig.json ./tsconfig.json
|
||||
COPY ./next.config.mjs ./next.config.mjs
|
||||
COPY ./public ./public
|
||||
|
||||
CMD ["npm", "run", "dev"]
|
||||
WORKDIR /
|
||||
|
|
240
README.md
240
README.md
|
@ -1,61 +1,198 @@
|
|||
# Tipi — A personal homeserver for everyone
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors-)
|
||||
|
||||
[](#contributors-)
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
[](https://github.com/runtipi/runtipi/blob/master/LICENSE)
|
||||
[](https://github.com/runtipi/runtipi/releases)
|
||||

|
||||
[](https://github.com/meienberger/runtipi/blob/master/LICENSE)
|
||||
[](https://github.com/meienberger/runtipi/releases)
|
||||

|
||||
[](https://hub.docker.com/r/meienberger/runtipi/)
|
||||
[](https://hub.docker.com/r/meienberger/runtipi/)
|
||||

|
||||
[](https://crowdin.com/project/runtipi)
|
||||
|
||||
> 💡 Tipi is built with TypeScript, Next.js app router and Drizzle ORM! If you want to collaborate on a cool project, join the discussion on Discord!
|
||||

|
||||
[](https://codecov.io/gh/meienberger/runtipi)
|
||||
|
||||
#### Join the discussion
|
||||
|
||||
[](https://discord.gg/Bu9qEPnHsc)
|
||||
[](https://matrix.to/#/#runtipi:matrix.org)
|
||||
|
||||

|
||||

|
||||
|
||||
> ⚠️ Tipi is still at an early stage of development and issues are to be expected. Feel free to open an issue or pull request if you find a bug.
|
||||
|
||||
Tipi is a personal homeserver orchestrator that makes it easy to manage and run multiple services on a single server. It is based on Docker and comes with a simple web interface to manage your services. Tipi is designed to be easy to use, so you don't have to worry about manual configuration or networking. Simply install Tipi on your server and use the web interface to add and manage services. You can see a list of available services in the [App Store repo](https://github.com/runtipi/runtipi-appstore) and request new ones if you don't see what you need. To get started, follow the installation instructions below.
|
||||
Tipi is a personal homeserver orchestrator. It is running docker containers under the hood and provides a simple web interface to manage them. Every service comes with an opinionated configuration in order to remove the need for manual configuration and network setup.
|
||||
|
||||
## Getting started
|
||||
Check our demo instance : **[demo.runtipi.com](https://demo.runtipi.com)** / username: **user@runtipi.com** / password: **runtipi**
|
||||
|
||||
Visit our website [runtipi.io](https://www.runtipi.io/docs/getting-started/installation?utm_source=github&utm_medium=README&utm_campaign=getting-started) for installation instructions, documentation and guides.
|
||||
## Apps available
|
||||
|
||||
## Demo
|
||||
- [Adguard Home](https://github.com/AdguardTeam/AdGuardHome) - Adguard Home DNS adblocker
|
||||
- [Booksonic](https://github.com/popeen) - A server for streaming your audiobooks
|
||||
- [BookStack](https://www.bookstackapp.com/) - BookStack is a self-hosted platform for organising and storing information.
|
||||
- [Calibre-Web](https://github.com/janeczku/calibre-web) - Web Ebook Reader
|
||||
- [Code-Server](https://github.com/coder/code-server) - Web VS Code
|
||||
- [Filebrowser](https://github.com/filebrowser/filebrowser) - Web File Browser
|
||||
- [Firefly III](https://github.com/firefly-iii/firefly-iii) - A personal finances manager
|
||||
- [FreshRSS](https://github.com/FreshRSS/FreshRSS) - A free, self-hostable RSS aggregator
|
||||
- [Ghost](https://github.com/TryGhost/Ghost) - Ghost - Turn your audience into a business
|
||||
- [Gitea](https://github.com/go-gitea/gitea) - Gitea - A painless self-hosted Git service
|
||||
- [Gotify](https://github.com/gotify/server) - Simple server for sending and receiving notification messages.
|
||||
- [Haven](https://github.com/havenweb/haven) - Haven is a self-hosted private blog and feedreader you can use instead of Facebook
|
||||
- [Homarr](https://github.com/ajnart/homarr) - A homepage for your server
|
||||
- [Home Assistant](https://github.com/home-assistant/core) - Open source home automation that puts local control and privacy first
|
||||
- [Immich](https://www.immich.app/) - Photo and video backup solution directly from your mobile phone
|
||||
- [Invidious](https://github.com/iv-org/invidious) - An alternative front-end to YouTube
|
||||
- [Jackett](https://github.com/Jackett/Jackett) - API Support for your favorite torrent trackers
|
||||
- [Jellyfin](https://github.com/jellyfin/jellyfin) - A media server for your home collection
|
||||
- [Joplin](https://github.com/laurent22/joplin) - Privacy focused note-taking app
|
||||
- [Libreddit](https://github.com/spikecodes/libreddit) - Private front-end for Reddit
|
||||
- [LibrePhotos](https://github.com/LibrePhotos/librephotos) - A self-hosted open source photo management service
|
||||
- [LibreTranslate](https://github.com/LibreTranslate/LibreTranslate) - Free and open source machine translation API
|
||||
- [Lidarr](https://github.com/Lidarr/Lidarr) - Looks and smells like Sonarr but made for music
|
||||
- [Mealie](https://github.com/hay-kot/mealie) - Self-hosted recipe manager and meal planner
|
||||
- [MoneroBlock](https://github.com/duggavo/MoneroBlock) - Decentralized and trustless Monero block explorer
|
||||
- [Monero Daemon](https://github.com/sethforprivacy/simple-monerod-docker) - Monero is a private, decentralized cryptocurrency that keeps your finances confidential and secure
|
||||
- [n8n](https://github.com/n8n-io/n8n) - Workflow Automation Tool
|
||||
- [Navidrome](https://github.com/navidrome/navidrome/) - Modern Music Server and Streamer compatible with Subsonic/Airsonic
|
||||
- [Nextcloud](https://github.com/nextcloud/server) - A safe home for all your data
|
||||
- [Nitter](https://github.com/zedeus/nitter) - Alternative Twitter front-end
|
||||
- [Node-RED](https://github.com/node-red/node-red) - Low-code programming for event-driven applications
|
||||
- [Overseerr](https://github.com/sct/overseerr) - Request management and media discovery tool for the Plex ecosystem
|
||||
- [Photoprism](https://github.com/photoprism/photoprism) - AI-Powered Photos App for the Decentralized Web. We are on a mission to protect your freedom and privacy.
|
||||
- [Pihole](https://github.com/pi-hole/pi-hole) - A black hole for Internet advertisements
|
||||
- [Plex](https://github.com/plexinc/pms-docker) - Stream Movies & TV Shows
|
||||
- [Portainer](https://github.com/portainer/portainer) - Making Docker and Kubernetes management easy
|
||||
- [PrivateBin](https://github.com/PrivateBin/PrivateBin) - A minimalist, open source online pastebin where the server has zero knowledge of pasted data
|
||||
- [Prowlarr](https://github.com/Prowlarr/Prowlarr/) - A torrent/usenet indexer manager/proxy
|
||||
- [ProxiTok](https://github.com/pablouser1/ProxiTok) - Open source alternative frontend for TikTok made using PHP
|
||||
- [qBittorrent](https://github.com/qbittorrent/qBittorrent) - Fast, easy, and free BitTorrent client
|
||||
- [Radarr](https://github.com/Radarr/Radarr) - Movie collection manager for Usenet and BitTorrent users
|
||||
- [Readarr](https://github.com/Readarr/Readarr) - Book Manager and Automation (Sonarr for Ebooks)
|
||||
- [Resilio Sync](https://github.com/bt-sync) - Fast, reliable, and simple file sync and share solution
|
||||
- [SearXNG](https://github.com/searxng/searxng) - Privacy-respecting, hackable metasearch engine
|
||||
- [Send](https://gitlab.com/timvisee/send) - Simple, private file sharing
|
||||
- [Sonarr](https://github.com/Sonarr/Sonarr) - TV show manager for Usenet and BitTorrent
|
||||
- [Syncthing](https://github.com/syncthing/syncthing) - Continuous File Synchronization
|
||||
- [Tailscale](https://github.com/tailscale/tailscale) - The easiest, most secure way to use WireGuard and 2FA
|
||||
- [Tautulli](https://github.com/Tautulli/Tautulli) - A Python based monitoring and tracking tool for Plex Media Server
|
||||
- [teddit](https://codeberg.org/teddit/teddit) - Alternative Reddit front-end focused on privacy
|
||||
- [Transmission](https://github.com/transmission/transmission) - Fast, easy, and free BitTorrent client
|
||||
- [Tube Archivist](https://github.com/tubearchivist/tubearchivist) - Your self-hosted YouTube media server
|
||||
- [Uptime Kuma](https://github.com/louislam/uptime-kuma) - A fancy self-hosted monitoring tool
|
||||
- [Vaultwarden](https://github.com/dani-garcia/vaultwarden) - Unofficial Bitwarden compatible server
|
||||
- [Wireguard Easy](https://github.com/WeeJeWel/wg-easy) - WireGuard VPN + Web-based Admin UI
|
||||
- [Your Spotify](https://github.com/Yooooomi/your_spotify) - Self hosted Spotify tracking dashboard
|
||||
- [Zerotier](https://www.zerotier.com) - Easy to use zero configuration VPN
|
||||
|
||||
You can try out a demo of Tipi at [demo.runtipi.io](https://demo.runtipi.io) using the following credentials:
|
||||
You can find and submit new apps inside of the [RunTipi Appstore](https://github.com/meienberger/runtipi-appstore).
|
||||
|
||||
username: user@runtipi.io
|
||||
password: password
|
||||
## 🛠 Installation
|
||||
|
||||
### Installation Requirements
|
||||
|
||||
Ubuntu 18.04 LTS or higher is recommended. However other major Linux distribution are supported but may lead to installation issues. Please file an issue if you encounter one.
|
||||
|
||||
### Step 1. Download Tipi
|
||||
|
||||
Run this in an empty directory where you want to install Tipi.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/meienberger/runtipi.git
|
||||
```
|
||||
|
||||
### Step 2. Run Tipi
|
||||
|
||||
cd into the downloaded directory and run the start script.
|
||||
|
||||
```bash
|
||||
cd runtipi
|
||||
sudo ./scripts/start.sh
|
||||
```
|
||||
|
||||
The script will prompt you the ip address of the dashboard once configured.
|
||||
Tipi will run by default on port 80. To select another port you can run the start script with the `--port` argument
|
||||
|
||||
```bash
|
||||
sudo ./scripts/start.sh --port 7000
|
||||
```
|
||||
|
||||
To stop Tipi, run the stop script.
|
||||
|
||||
```bash
|
||||
sudo ./scripts/stop.sh
|
||||
```
|
||||
|
||||
### Custom settings
|
||||
|
||||
You can change the default settings by creating a `settings.json` file. The file should be located in the `state` directory. This file will make your changes persist across restarts. Example file:
|
||||
|
||||
```json
|
||||
{
|
||||
"dnsIp": "9.9.9.9",
|
||||
"domain": "mydomain.com"
|
||||
}
|
||||
```
|
||||
|
||||
Available settings:
|
||||
|
||||
- `dnsIp` - The IP address of the DNS server to use. Default: `9.9.9.9`
|
||||
- `domain` - The domain name to use for the dashboard. Default: `localhost`
|
||||
- `port` - The port to use for the dashboard. Default: `80`
|
||||
- `sslPort` - The port to use for the dashboard with SSL. Default: `443`
|
||||
- `listenIp` - The IP address to listen on. Default: `automatically detected`
|
||||
- `storagePath` - The path to use for storing data. Default: `runtipi/app-data`
|
||||
|
||||
### Linking a domain to your dashboard
|
||||
|
||||
If you want to link a domain to your dashboard, you can do so by providing the `--domain` option in the start script.
|
||||
|
||||
```bash
|
||||
sudo ./scripts/start.sh --domain mydomain.com
|
||||
```
|
||||
|
||||
You can also specify it in the `settings.json` file as shown in the previous section to keep the setting saved across restarts.
|
||||
|
||||
A Let's Encrypt certificate will be generated and installed automatically. Make sure to have ports 80 and 443 open on your firewall and that your domain has an **A** record pointing to your server IP.
|
||||
|
||||
Please note that this setting will only expose the dashboard. If you want to expose other apps, you need to configure them individually. You cannot use the `--domain` option to expose apps.
|
||||
|
||||
This option will only work if you keep the default port 80 and 443 for the dashboard.
|
||||
|
||||
### Uninstalling Tipi
|
||||
|
||||
Make sure Tipi is completely stopped and then remove the `runtipi` directory.
|
||||
|
||||
```bash
|
||||
sudo ./scripts/stop.sh
|
||||
cd ..
|
||||
sudo rm -rf runtipi
|
||||
```
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
For a detailed guide on how to install Tipi. This amazing article by @kycfree [Running a Home Server with Tipi](https://kyc3.life/running-a-home-server-with-tipi/)
|
||||
For a detailed guide on how to install Tipi. This amazing article by @kycfree1 [Running a Home Server with Tipi](https://kyc3.life/running-a-home-server-with-tipi/)
|
||||
|
||||
You can find more documentation and tutorials / FAQ on [runtipi.io](https://www.runtipi.io/docs/introduction?utm_source=github&utm_medium=README&utm_campaign=main-repo-docs)
|
||||
You can find more documentation and tutorials / FAQ in the [Wiki](https://github.com/meienberger/runtipi/wiki).
|
||||
|
||||
## ❤️ Contributing
|
||||
|
||||
Tipi is made to be very easy to plug in new apps. We welcome and appreciate new contributions.
|
||||
|
||||
If you want to add a new app or feature, you can follow the [Contribution guide](https://www.runtipi.io/docs/contributing/adding-a-new-app) for instructions on how to do so.
|
||||
If you want to add a new app or feature, you can follow the [Contribution guide](https://github.com/meienberger/runtipi/wiki/Adding-your-own-app) for instructions on how to do so.
|
||||
|
||||
We are looking for contributions of all kinds. If you know design, development, or have ideas for new features, please get in touch.
|
||||
|
||||
## 📜 License
|
||||
|
||||
[](https://github.com/runtipi/runtipi/blob/master/LICENSE)
|
||||
[](https://github.com/meienberger/runtipi/blob/master/LICENSE)
|
||||
|
||||
Tipi is licensed under the GNU General Public License v3.0. TL;DR — You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build & install instructions.
|
||||
|
||||
The bash script `app.sh` located in the `scripts` folder contains some snippets from [Umbrel](https://github.com/getumbrel/umbrel)'s code. Therefore some parts of the code are licensed under the PolyForm Noncommercial License 1.0.0 license. You can for now consider the whole file under this license. We are actively working on re-writing those parts in order to make them available under the GPL license like the rest of our code.
|
||||
|
||||
## 🗣 Community
|
||||
|
||||
- [Matrix](https://matrix.to/#/#runtipi:matrix.org)<br />
|
||||
|
@ -73,58 +210,25 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://meienberger.dev/"><img src="https://avatars.githubusercontent.com/u/47644445?v=4?s=100" width="100px;" alt="Nicolas Meienberger"/><br /><sub><b>Nicolas Meienberger</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=meienberger" title="Code">💻</a> <a href="#infra-meienberger" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/runtipi/runtipi/commits?author=meienberger" title="Tests">⚠️</a> <a href="https://github.com/runtipi/runtipi/commits?author=meienberger" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ArneNaessens"><img src="https://avatars.githubusercontent.com/u/16622722?v=4?s=100" width="100px;" alt="ArneNaessens"/><br /><sub><b>ArneNaessens</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=ArneNaessens" title="Code">💻</a> <a href="#ideas-ArneNaessens" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/runtipi/runtipi/commits?author=ArneNaessens" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DrMxrcy"><img src="https://avatars.githubusercontent.com/u/58747968?v=4?s=100" width="100px;" alt="DrMxrcy"/><br /><sub><b>DrMxrcy</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=DrMxrcy" title="Code">💻</a> <a href="#ideas-DrMxrcy" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/runtipi/runtipi/commits?author=DrMxrcy" title="Tests">⚠️</a> <a href="#content-DrMxrcy" title="Content">🖋</a> <a href="#promotion-DrMxrcy" title="Promotion">📣</a> <a href="#question-DrMxrcy" title="Answering Questions">💬</a> <a href="https://github.com/runtipi/runtipi/pulls?q=is%3Apr+reviewed-by%3ADrMxrcy" title="Reviewed Pull Requests">👀</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://cobre.dev"><img src="https://avatars.githubusercontent.com/u/36574329?v=4?s=100" width="100px;" alt="Cooper"/><br /><sub><b>Cooper</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=CobreDev" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JTruj1ll0923"><img src="https://avatars.githubusercontent.com/u/6656643?v=4?s=100" width="100px;" alt="JTruj1ll0923"/><br /><sub><b>JTruj1ll0923</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=JTruj1ll0923" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Stetsed"><img src="https://avatars.githubusercontent.com/u/33891782?v=4?s=100" width="100px;" alt="Stetsed"/><br /><sub><b>Stetsed</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=Stetsed" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/blushell"><img src="https://avatars.githubusercontent.com/u/3621606?v=4?s=100" width="100px;" alt="Jones_Town"/><br /><sub><b>Jones_Town</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=blushell" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://meienberger.dev/"><img src="https://avatars.githubusercontent.com/u/47644445?v=4?s=100" width="100px;" alt="Nicolas Meienberger"/><br /><sub><b>Nicolas Meienberger</b></sub></a><br /><a href="https://github.com/meienberger/runtipi/commits?author=meienberger" title="Code">💻</a> <a href="#infra-meienberger" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://github.com/meienberger/runtipi/commits?author=meienberger" title="Tests">⚠️</a> <a href="https://github.com/meienberger/runtipi/commits?author=meienberger" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/ArneNaessens"><img src="https://avatars.githubusercontent.com/u/16622722?v=4?s=100" width="100px;" alt="ArneNaessens"/><br /><sub><b>ArneNaessens</b></sub></a><br /><a href="https://github.com/meienberger/runtipi/commits?author=ArneNaessens" title="Code">💻</a> <a href="#ideas-ArneNaessens" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/meienberger/runtipi/commits?author=ArneNaessens" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/DrMxrcy"><img src="https://avatars.githubusercontent.com/u/58747968?v=4?s=100" width="100px;" alt="DrMxrcy"/><br /><sub><b>DrMxrcy</b></sub></a><br /><a href="https://github.com/meienberger/runtipi/commits?author=DrMxrcy" title="Code">💻</a> <a href="#ideas-DrMxrcy" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/meienberger/runtipi/commits?author=DrMxrcy" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://cobre.dev"><img src="https://avatars.githubusercontent.com/u/36574329?v=4?s=100" width="100px;" alt="Cooper"/><br /><sub><b>Cooper</b></sub></a><br /><a href="https://github.com/meienberger/runtipi/commits?author=CobreDev" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/JTruj1ll0923"><img src="https://avatars.githubusercontent.com/u/6656643?v=4?s=100" width="100px;" alt="JTruj1ll0923"/><br /><sub><b>JTruj1ll0923</b></sub></a><br /><a href="https://github.com/meienberger/runtipi/commits?author=JTruj1ll0923" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/Stetsed"><img src="https://avatars.githubusercontent.com/u/33891782?v=4?s=100" width="100px;" alt="Stetsed"/><br /><sub><b>Stetsed</b></sub></a><br /><a href="https://github.com/meienberger/runtipi/commits?author=Stetsed" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/blushell"><img src="https://avatars.githubusercontent.com/u/3621606?v=4?s=100" width="100px;" alt="Jones_Town"/><br /><sub><b>Jones_Town</b></sub></a><br /><a href="https://github.com/meienberger/runtipi/commits?author=blushell" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://rushichaudhari.github.io/"><img src="https://avatars.githubusercontent.com/u/6279035?v=4?s=100" width="100px;" alt="Rushi Chaudhari"/><br /><sub><b>Rushi Chaudhari</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=rushic24" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rblaine95"><img src="https://avatars.githubusercontent.com/u/4052340?v=4?s=100" width="100px;" alt="Robert Blaine"/><br /><sub><b>Robert Blaine</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=rblaine95" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://sethforprivacy.com"><img src="https://avatars.githubusercontent.com/u/40500387?v=4?s=100" width="100px;" alt="Seth For Privacy"/><br /><sub><b>Seth For Privacy</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=sethforprivacy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hqwuzhaoyi"><img src="https://avatars.githubusercontent.com/u/44605072?v=4?s=100" width="100px;" alt="Prajna"/><br /><sub><b>Prajna</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=hqwuzhaoyi" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/justincmoy"><img src="https://avatars.githubusercontent.com/u/14875982?v=4?s=100" width="100px;" alt="Justin Moy"/><br /><sub><b>Justin Moy</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=justincmoy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dextreem"><img src="https://avatars.githubusercontent.com/u/11060652?v=4?s=100" width="100px;" alt="dextreem"/><br /><sub><b>dextreem</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=dextreem" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/iBicha"><img src="https://avatars.githubusercontent.com/u/17722782?v=4?s=100" width="100px;" alt="Brahim Hadriche"/><br /><sub><b>Brahim Hadriche</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=iBicha" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://rushichaudhari.github.io/"><img src="https://avatars.githubusercontent.com/u/6279035?v=4?s=100" width="100px;" alt="Rushi Chaudhari"/><br /><sub><b>Rushi Chaudhari</b></sub></a><br /><a href="https://github.com/meienberger/runtipi/commits?author=rushic24" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/rblaine95"><img src="https://avatars.githubusercontent.com/u/4052340?v=4?s=100" width="100px;" alt="Robert Blaine"/><br /><sub><b>Robert Blaine</b></sub></a><br /><a href="https://github.com/meienberger/runtipi/commits?author=rblaine95" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://sethforprivacy.com"><img src="https://avatars.githubusercontent.com/u/40500387?v=4?s=100" width="100px;" alt="Seth For Privacy"/><br /><sub><b>Seth For Privacy</b></sub></a><br /><a href="https://github.com/meienberger/runtipi/commits?author=sethforprivacy" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/hqwuzhaoyi"><img src="https://avatars.githubusercontent.com/u/44605072?v=4?s=100" width="100px;" alt="Prajna"/><br /><sub><b>Prajna</b></sub></a><br /><a href="https://github.com/meienberger/runtipi/commits?author=hqwuzhaoyi" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/justincmoy"><img src="https://avatars.githubusercontent.com/u/14875982?v=4?s=100" width="100px;" alt="Justin Moy"/><br /><sub><b>Justin Moy</b></sub></a><br /><a href="https://github.com/meienberger/runtipi/commits?author=justincmoy" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/dextreem"><img src="https://avatars.githubusercontent.com/u/11060652?v=4?s=100" width="100px;" alt="dextreem"/><br /><sub><b>dextreem</b></sub></a><br /><a href="https://github.com/meienberger/runtipi/commits?author=dextreem" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/iBicha"><img src="https://avatars.githubusercontent.com/u/17722782?v=4?s=100" width="100px;" alt="Brahim Hadriche"/><br /><sub><b>Brahim Hadriche</b></sub></a><br /><a href="https://github.com/meienberger/runtipi/commits?author=iBicha" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://andrewbrereton.com"><img src="https://avatars.githubusercontent.com/u/682893?v=4?s=100" width="100px;" alt="Andrew Brereton"/><br /><sub><b>Andrew Brereton</b></sub></a><br /><a href="#content-andrewbrereton" title="Content">🖋</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://fsackur.github.io/"><img src="https://avatars.githubusercontent.com/u/3678789?v=4?s=100" width="100px;" alt="Freddie Sackur"/><br /><sub><b>Freddie Sackur</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=fsackur" title="Code">💻</a> <a href="https://github.com/runtipi/runtipi/commits?author=fsackur" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://innocentius.github.io"><img src="https://avatars.githubusercontent.com/u/5344432?v=4?s=100" width="100px;" alt="Innocentius"/><br /><sub><b>Innocentius</b></sub></a><br /><a href="#translation-innocentius" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/TetrisIQ"><img src="https://avatars.githubusercontent.com/u/24246993?v=4?s=100" width="100px;" alt="Alex"/><br /><sub><b>Alex</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=TetrisIQ" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://ryanc.cc"><img src="https://avatars.githubusercontent.com/u/21301288?v=4?s=100" width="100px;" alt="Ryan Wang"/><br /><sub><b>Ryan Wang</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=ruibaby" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/simonandr"><img src="https://avatars.githubusercontent.com/u/48092304?v=4?s=100" width="100px;" alt="simonandr"/><br /><sub><b>simonandr</b></sub></a><br /><a href="#content-simonandr" title="Content">🖋</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/demizeu"><img src="https://avatars.githubusercontent.com/u/121183951?v=4?s=100" width="100px;" alt="iepure"/><br /><sub><b>iepure</b></sub></a><br /><a href="#translation-demizeu" title="Translation">🌍</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SergeyKodolov"><img src="https://avatars.githubusercontent.com/u/35339452?v=4?s=100" width="100px;" alt="Sergey Kodolov"/><br /><sub><b>Sergey Kodolov</b></sub></a><br /><a href="#translation-SergeyKodolov" title="Translation">🌍</a> <a href="https://github.com/runtipi/runtipi/commits?author=SergeyKodolov" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/sclaren"><img src="https://avatars.githubusercontent.com/u/915292?v=4?s=100" width="100px;" alt="sclaren"/><br /><sub><b>sclaren</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=sclaren" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mcmeel"><img src="https://avatars.githubusercontent.com/u/13773536?v=4?s=100" width="100px;" alt="mcmeel"/><br /><sub><b>mcmeel</b></sub></a><br /><a href="#question-mcmeel" title="Answering Questions">💬</a> <a href="#ideas-mcmeel" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/runtipi/runtipi/commits?author=mcmeel" title="Code">💻</a> <a href="https://github.com/runtipi/runtipi/commits?author=mcmeel" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/NoisyFridge"><img src="https://avatars.githubusercontent.com/u/73795785?v=4?s=100" width="100px;" alt="NoisyFridge"/><br /><sub><b>NoisyFridge</b></sub></a><br /><a href="#translation-NoisyFridge" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Bvoxl"><img src="https://avatars.githubusercontent.com/u/67489519?v=4?s=100" width="100px;" alt="Bvoxl"/><br /><sub><b>Bvoxl</b></sub></a><br /><a href="#translation-Bvoxl" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/m-lab-0"><img src="https://avatars.githubusercontent.com/u/116570617?v=4?s=100" width="100px;" alt="m-lab-0"/><br /><sub><b>m-lab-0</b></sub></a><br /><a href="#translation-m-lab-0" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dannkunt"><img src="https://avatars.githubusercontent.com/u/32395839?v=4?s=100" width="100px;" alt="dannkunt"/><br /><sub><b>dannkunt</b></sub></a><br /><a href="#translation-dannkunt" title="Translation">🌍</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Schmanko"><img src="https://avatars.githubusercontent.com/u/94195393?v=4?s=100" width="100px;" alt="Schmanko"/><br /><sub><b>Schmanko</b></sub></a><br /><a href="#translation-Schmanko" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://micro.nghialele.com"><img src="https://avatars.githubusercontent.com/u/129353223?v=4?s=100" width="100px;" alt="Nghia Lele"/><br /><sub><b>Nghia Lele</b></sub></a><br /><a href="#translation-nghialele" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/amusingimpala75"><img src="https://avatars.githubusercontent.com/u/69653100?v=4?s=100" width="100px;" alt="amusingimpala75"/><br /><sub><b>amusingimpala75</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=amusingimpala75" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://m1n.omg.lol"><img src="https://avatars.githubusercontent.com/u/54779580?v=4?s=100" width="100px;" alt="David"/><br /><sub><b>David</b></sub></a><br /><a href="#translation-M1n-4d316e" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/steveiliop56"><img src="https://avatars.githubusercontent.com/u/106091011?v=4?s=100" width="100px;" alt="Stavros Iliopoulos"/><br /><sub><b>Stavros Iliopoulos</b></sub></a><br /><a href="#translation-steveiliop56" title="Translation">🌍</a> <a href="https://github.com/runtipi/runtipi/commits?author=steveiliop56" title="Code">💻</a> <a href="https://github.com/runtipi/runtipi/commits?author=steveiliop56" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/loxiry"><img src="https://avatars.githubusercontent.com/u/86959495?v=4?s=100" width="100px;" alt="loxiry"/><br /><sub><b>loxiry</b></sub></a><br /><a href="#translation-loxiry" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JigSawFr"><img src="https://avatars.githubusercontent.com/u/5781907?v=4?s=100" width="100px;" alt="JigSaw"/><br /><sub><b>JigSaw</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=JigSawFr" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DireMunchkin"><img src="https://avatars.githubusercontent.com/u/1665676?v=4?s=100" width="100px;" alt="DireMunchkin"/><br /><sub><b>DireMunchkin</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=DireMunchkin" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/FabioCingottini"><img src="https://avatars.githubusercontent.com/u/32102735?v=4?s=100" width="100px;" alt="Fabio Cingottini"/><br /><sub><b>Fabio Cingottini</b></sub></a><br /><a href="#translation-FabioCingottini" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/itsrllyhim"><img src="https://avatars.githubusercontent.com/u/143047010?v=4?s=100" width="100px;" alt="him"/><br /><sub><b>him</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=itsrllyhim" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://cchalop1.com"><img src="https://avatars.githubusercontent.com/u/28163855?v=4?s=100" width="100px;" alt="CHALOPIN Clément"/><br /><sub><b>CHALOPIN Clément</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=cchalop1" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/geetansh"><img src="https://avatars.githubusercontent.com/u/9976198?v=4?s=100" width="100px;" alt="Geetansh Jindal"/><br /><sub><b>Geetansh Jindal</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=geetansh" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/0livier"><img src="https://avatars.githubusercontent.com/u/10607?v=4?s=100" width="100px;" alt="Olivier Garcia"/><br /><sub><b>Olivier Garcia</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=0livier" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/qcoudeyr"><img src="https://avatars.githubusercontent.com/u/124463277?v=4?s=100" width="100px;" alt="qcoudeyr"/><br /><sub><b>qcoudeyr</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=qcoudeyr" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://andrewbrereton.com"><img src="https://avatars.githubusercontent.com/u/682893?v=4?s=100" width="100px;" alt="Andrew Brereton"/><br /><sub><b>Andrew Brereton</b></sub></a><br /><a href="#content-andrewbrereton" title="Content">🖋</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
import { fs, vol } from 'memfs';
|
||||
|
||||
const copyFolderRecursiveSync = (src: string, dest: string) => {
|
||||
const exists = vol.existsSync(src);
|
||||
const stats = vol.statSync(src);
|
||||
const isDirectory = exists && stats.isDirectory();
|
||||
if (isDirectory) {
|
||||
vol.mkdirSync(dest, { recursive: true });
|
||||
vol.readdirSync(src).forEach((childItemName) => {
|
||||
copyFolderRecursiveSync(`${src}/${childItemName}`, `${dest}/${childItemName}`);
|
||||
});
|
||||
} else {
|
||||
vol.copyFileSync(src, dest);
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
...fs,
|
||||
copySync: (src: string, dest: string) => {
|
||||
copyFolderRecursiveSync(src, dest);
|
||||
},
|
||||
__resetAllMocks: () => {
|
||||
vol.reset();
|
||||
},
|
||||
__applyMockFiles: (newMockFiles: Record<string, string>) => {
|
||||
// Create folder tree
|
||||
vol.fromJSON(newMockFiles, 'utf8');
|
||||
},
|
||||
__createMockFiles: (newMockFiles: Record<string, string>) => {
|
||||
vol.reset();
|
||||
// Create folder tree
|
||||
vol.fromJSON(newMockFiles, 'utf8');
|
||||
},
|
||||
__printVol: () => console.log(vol.toTree()),
|
||||
};
|
|
@ -1,29 +0,0 @@
|
|||
const values = new Map();
|
||||
const expirations = new Map();
|
||||
|
||||
export const createClient = jest.fn(() => {
|
||||
return {
|
||||
isOpen: true,
|
||||
connect: jest.fn(),
|
||||
set: (key: string, value: string, exp: number) => {
|
||||
values.set(key, value);
|
||||
expirations.set(key, exp);
|
||||
},
|
||||
get: (key: string) => values.get(key),
|
||||
quit: jest.fn(),
|
||||
del: (key: string) => values.delete(key),
|
||||
ttl: (key: string) => expirations.get(key),
|
||||
on: jest.fn(),
|
||||
keys: (key: string) => {
|
||||
const keyprefix = key.substring(0, key.length - 1);
|
||||
const keys = [];
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const [k] of values) {
|
||||
if (k.startsWith(keyprefix)) {
|
||||
keys.push(k);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
},
|
||||
};
|
||||
});
|
12
codecov.yml
12
codecov.yml
|
@ -1,12 +0,0 @@
|
|||
ignore:
|
||||
- 'public'
|
||||
- 'scripts'
|
||||
- 'templates'
|
||||
- 'screenshots'
|
||||
- '**/*.json'
|
||||
- '**/tests/**'
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
informational: true
|
3
commitlint.config.js
Normal file
3
commitlint.config.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
extends: ["@commitlint/config-conventional"],
|
||||
};
|
|
@ -1,3 +0,0 @@
|
|||
files:
|
||||
- source: /src/client/messages/en.json
|
||||
translation: /src/client/messages/%locale%.json
|
|
@ -1,19 +1,20 @@
|
|||
version: '3.7'
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
tipi-reverse-proxy:
|
||||
container_name: tipi-reverse-proxy
|
||||
reverse-proxy:
|
||||
container_name: reverse-proxy
|
||||
image: traefik:v2.8
|
||||
restart: on-failure
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
- ${NGINX_PORT-80}:80
|
||||
- ${NGINX_PORT_SSL-443}:443
|
||||
- 8080:8080
|
||||
command: --providers.docker
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ${PWD}/traefik:/root/.config
|
||||
- ${PWD}/traefik/shared:/shared
|
||||
- ${PWD}/traefik/letsencrypt:/letsencrypt
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
|
@ -23,7 +24,7 @@ services:
|
|||
restart: unless-stopped
|
||||
stop_grace_period: 1m
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
- ./data/postgres:/var/lib/postgresql/data
|
||||
ports:
|
||||
- 5432:5432
|
||||
environment:
|
||||
|
@ -31,7 +32,7 @@ services:
|
|||
POSTGRES_USER: tipi
|
||||
POSTGRES_DB: tipi
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -d tipi -U tipi']
|
||||
test: ["CMD-SHELL", "pg_isready -d tipi -U tipi"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 120
|
||||
|
@ -40,110 +41,101 @@ services:
|
|||
|
||||
tipi-redis:
|
||||
container_name: tipi-redis
|
||||
image: redis:7.2.0
|
||||
image: redis:alpine
|
||||
restart: unless-stopped
|
||||
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||
ports:
|
||||
- 6379:6379
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
healthcheck:
|
||||
test: ['CMD', 'redis-cli', 'ping']
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 120
|
||||
- ./data/redis:/data
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
tipi-worker:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./packages/worker/Dockerfile.dev
|
||||
container_name: tipi-worker
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '-f', 'http://localhost:3000/healthcheck']
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 120
|
||||
start_period: 5s
|
||||
depends_on:
|
||||
tipi-db:
|
||||
condition: service_healthy
|
||||
tipi-redis:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
NODE_ENV: development
|
||||
volumes:
|
||||
# Dev mode
|
||||
- ${PWD}/packages/worker/src:/app/packages/worker/src
|
||||
# Production mode
|
||||
- /proc:/host/proc:ro
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ${PWD}/.env:/app/.env
|
||||
- ${PWD}/state:/app/state
|
||||
- ${PWD}/repos:/app/repos
|
||||
- ${PWD}/apps:/app/apps
|
||||
- ${STORAGE_PATH:-$PWD}/app-data:/storage/app-data
|
||||
- ${PWD}/logs:/app/logs
|
||||
- ${PWD}/traefik:/app/traefik
|
||||
- ${PWD}/user-config:/app/user-config
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
tipi-dashboard:
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dev
|
||||
container_name: tipi-dashboard
|
||||
command: /bin/sh -c "cd /api && npm run build && npm run dev"
|
||||
depends_on:
|
||||
tipi-db:
|
||||
condition: service_healthy
|
||||
tipi-redis:
|
||||
condition: service_healthy
|
||||
tipi-worker:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
NODE_ENV: development
|
||||
networks:
|
||||
- tipi_main_network
|
||||
container_name: api
|
||||
ports:
|
||||
- 3000:3000
|
||||
- 3001:3001
|
||||
volumes:
|
||||
# - /dashboard/node_modules
|
||||
# - /dashboard/.next
|
||||
- ${PWD}/.env:/runtipi/.env
|
||||
- ${PWD}/src:/app/src
|
||||
- ${PWD}/packages:/app/packages
|
||||
- ${PWD}/state:/runtipi/state
|
||||
- ${PWD}/repos:/runtipi/repos:ro
|
||||
- ${PWD}/apps:/runtipi/apps
|
||||
- ${PWD}/state:/runtipi/state
|
||||
- ${PWD}/packages/system-api/src:/api/src
|
||||
- ${STORAGE_PATH}:/app/storage
|
||||
- ${PWD}/logs:/app/logs
|
||||
- ${PWD}/traefik:/runtipi/traefik
|
||||
- ${STORAGE_PATH:-$PWD}:/app/storage
|
||||
- ${PWD}/.env.dev:/runtipi/.env
|
||||
# - /api/node_modules
|
||||
environment:
|
||||
INTERNAL_IP: ${INTERNAL_IP}
|
||||
TIPI_VERSION: ${TIPI_VERSION}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
NGINX_PORT: ${NGINX_PORT}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_USERNAME: tipi
|
||||
POSTGRES_DBNAME: tipi
|
||||
POSTGRES_HOST: tipi-db
|
||||
APPS_REPO_ID: ${APPS_REPO_ID}
|
||||
APPS_REPO_URL: ${APPS_REPO_URL}
|
||||
DOMAIN: ${DOMAIN}
|
||||
ARCHITECTURE: ${ARCHITECTURE}
|
||||
networks:
|
||||
- tipi_main_network
|
||||
labels:
|
||||
traefik.enable: true
|
||||
# Web
|
||||
traefik.http.routers.api.rule: PathPrefix(`/api`)
|
||||
traefik.http.routers.api.service: api
|
||||
traefik.http.routers.api.entrypoints: web
|
||||
traefik.http.routers.api.middlewares: api-stripprefix
|
||||
traefik.http.services.api.loadbalancer.server.port: 3001
|
||||
# Middlewares
|
||||
traefik.http.middlewares.api-stripprefix.stripprefix.prefixes: /api
|
||||
|
||||
dashboard:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dev
|
||||
command: /bin/sh -c "cd /dashboard && npm run dev"
|
||||
container_name: dashboard
|
||||
depends_on:
|
||||
api:
|
||||
condition: service_started
|
||||
ports:
|
||||
- 3000:3000
|
||||
networks:
|
||||
- tipi_main_network
|
||||
environment:
|
||||
INTERNAL_IP: ${INTERNAL_IP}
|
||||
DOMAIN: ${DOMAIN}
|
||||
NGINX_PORT: ${NGINX_PORT-80}
|
||||
volumes:
|
||||
- ${PWD}/packages/dashboard/src:/dashboard/src
|
||||
# - /dashboard/node_modules
|
||||
# - /dashboard/.next
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.dashboard-redirect.rule: PathPrefix("/")
|
||||
traefik.http.routers.dashboard-redirect.entrypoints: web
|
||||
traefik.http.routers.dashboard-redirect.middlewares: redirect-middleware
|
||||
traefik.http.routers.dashboard-redirect.service: dashboard
|
||||
traefik.http.services.dashboard-redirect.loadbalancer.server.port: 3000
|
||||
|
||||
# Web
|
||||
traefik.http.routers.dashboard.rule: PathPrefix("/dashboard")
|
||||
traefik.http.routers.dashboard.service: dashboard
|
||||
traefik.http.routers.dashboard.entrypoints: web
|
||||
traefik.http.services.dashboard.loadbalancer.server.port: 3000
|
||||
traefik.http.middlewares.redirect-to-https.redirectscheme.scheme: https
|
||||
# Local domain
|
||||
traefik.http.routers.dashboard-local-insecure.rule: Host(`${LOCAL_DOMAIN}`)
|
||||
traefik.http.routers.dashboard-local-insecure.entrypoints: web
|
||||
traefik.http.routers.dashboard-local-insecure.service: dashboard
|
||||
traefik.http.routers.dashboard-local-insecure.middlewares: redirect-to-https
|
||||
# secure
|
||||
traefik.http.routers.dashboard-local.rule: Host(`${LOCAL_DOMAIN}`)
|
||||
traefik.http.routers.dashboard-local.entrypoints: websecure
|
||||
traefik.http.routers.dashboard-local.tls: true
|
||||
traefik.http.routers.dashboard-local.service: dashboard
|
||||
# Middlewares
|
||||
traefik.http.middlewares.redirect-middleware.redirectregex.regex: .*
|
||||
traefik.http.middlewares.redirect-middleware.redirectregex.replacement: /dashboard
|
||||
|
||||
networks:
|
||||
tipi_main_network:
|
||||
driver: bridge
|
||||
name: runtipi_tipi_main_network
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
redisdata:
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: 10.21.21.0/24
|
||||
|
|
|
@ -1,146 +0,0 @@
|
|||
version: '3.7'
|
||||
|
||||
services:
|
||||
tipi-reverse-proxy:
|
||||
container_name: tipi-reverse-proxy
|
||||
image: traefik:v2.8
|
||||
restart: on-failure
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
- 8080:8080
|
||||
command: --providers.docker
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ${PWD}/traefik:/root/.config
|
||||
- ${PWD}/traefik/shared:/shared
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
tipi-db:
|
||||
container_name: tipi-db
|
||||
image: postgres:14
|
||||
restart: unless-stopped
|
||||
stop_grace_period: 1m
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
ports:
|
||||
- 5432:5432
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_USER: tipi
|
||||
POSTGRES_DB: tipi
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -d tipi -U tipi']
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 120
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
tipi-redis:
|
||||
container_name: tipi-redis
|
||||
image: redis:7.2.0
|
||||
restart: unless-stopped
|
||||
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||
ports:
|
||||
- 6379:6379
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
healthcheck:
|
||||
test: ['CMD', 'redis-cli', 'ping']
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 120
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
tipi-worker:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./packages/worker/Dockerfile
|
||||
container_name: tipi-worker
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '-f', 'http://localhost:3000/healthcheck']
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 120
|
||||
start_period: 5s
|
||||
depends_on:
|
||||
tipi-db:
|
||||
condition: service_healthy
|
||||
tipi-redis:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
volumes:
|
||||
- /proc:/host/proc
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ${PWD}/.env:/app/.env
|
||||
- ${PWD}/state:/app/state
|
||||
- ${PWD}/repos:/app/repos
|
||||
- ${PWD}/apps:/app/apps
|
||||
- ${STORAGE_PATH:-$PWD}/app-data:/storage/app-data
|
||||
- ${PWD}/logs:/app/logs
|
||||
- ${PWD}/traefik:/app/traefik
|
||||
- ${PWD}/user-config:/app/user-config
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
tipi-dashboard:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: tipi-dashboard
|
||||
depends_on:
|
||||
tipi-db:
|
||||
condition: service_healthy
|
||||
tipi-redis:
|
||||
condition: service_healthy
|
||||
tipi-worker:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
NODE_ENV: development
|
||||
networks:
|
||||
- tipi_main_network
|
||||
ports:
|
||||
- 3000:3000
|
||||
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
|
||||
- ${STORAGE_PATH}:/app/storage
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.services.dashboard.loadbalancer.server.port: 3000
|
||||
traefik.http.middlewares.redirect-to-https.redirectscheme.scheme: https
|
||||
# Local ip
|
||||
traefik.http.routers.dashboard.rule: PathPrefix("/")
|
||||
traefik.http.routers.dashboard.service: dashboard
|
||||
traefik.http.routers.dashboard.entrypoints: web
|
||||
# Local domain
|
||||
traefik.http.routers.dashboard-local-insecure.rule: Host(`${LOCAL_DOMAIN}`)
|
||||
traefik.http.routers.dashboard-local-insecure.entrypoints: web
|
||||
traefik.http.routers.dashboard-local-insecure.service: dashboard
|
||||
traefik.http.routers.dashboard-local-insecure.middlewares: redirect-to-https
|
||||
# secure
|
||||
traefik.http.routers.dashboard-local.rule: Host(`${LOCAL_DOMAIN}`)
|
||||
traefik.http.routers.dashboard-local.entrypoints: websecure
|
||||
traefik.http.routers.dashboard-local.tls: true
|
||||
traefik.http.routers.dashboard-local.service: dashboard
|
||||
|
||||
networks:
|
||||
tipi_main_network:
|
||||
driver: bridge
|
||||
name: runtipi_tipi_main_network
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
redisdata:
|
146
docker-compose.rc.yml
Normal file
146
docker-compose.rc.yml
Normal file
|
@ -0,0 +1,146 @@
|
|||
version: "3.7"
|
||||
|
||||
services:
|
||||
reverse-proxy:
|
||||
container_name: reverse-proxy
|
||||
image: traefik:v2.8
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- ${NGINX_PORT-80}:80
|
||||
- ${NGINX_PORT_SSL-443}:443
|
||||
- 8080:8080
|
||||
command: --providers.docker
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ${PWD}/traefik:/root/.config
|
||||
- ${PWD}/traefik/shared:/shared
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
tipi-db:
|
||||
container_name: tipi-db
|
||||
image: postgres:14
|
||||
restart: unless-stopped
|
||||
stop_grace_period: 1m
|
||||
volumes:
|
||||
- ./data/postgres:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_USER: tipi
|
||||
POSTGRES_DB: tipi
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -d tipi -U tipi"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 120
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
tipi-redis:
|
||||
container_name: tipi-redis
|
||||
image: redis:alpine
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data/redis:/data
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
api:
|
||||
image: meienberger/runtipi:rc-${TIPI_VERSION}
|
||||
command: /bin/sh -c "cd /api && npm run start"
|
||||
container_name: api
|
||||
depends_on:
|
||||
tipi-db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ${PWD}/repos:/runtipi/repos:ro
|
||||
- ${PWD}/apps:/runtipi/apps
|
||||
- ${PWD}/state:/runtipi/state
|
||||
- ${PWD}/logs:/app/logs
|
||||
- ${STORAGE_PATH}:/app/storage
|
||||
- ${PWD}/.env:/runtipi/.env:ro
|
||||
environment:
|
||||
INTERNAL_IP: ${INTERNAL_IP}
|
||||
TIPI_VERSION: ${TIPI_VERSION}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
NGINX_PORT: ${NGINX_PORT}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_USERNAME: tipi
|
||||
POSTGRES_DBNAME: tipi
|
||||
POSTGRES_HOST: tipi-db
|
||||
NODE_ENV: production
|
||||
APPS_REPO_ID: ${APPS_REPO_ID}
|
||||
APPS_REPO_URL: ${APPS_REPO_URL}
|
||||
DOMAIN: ${DOMAIN}
|
||||
ARCHITECTURE: ${ARCHITECTURE}
|
||||
networks:
|
||||
- tipi_main_network
|
||||
labels:
|
||||
traefik.enable: true
|
||||
# Web
|
||||
traefik.http.routers.api.rule: PathPrefix(`/api`)
|
||||
traefik.http.routers.api.service: api
|
||||
traefik.http.routers.api.entrypoints: web
|
||||
traefik.http.routers.api.middlewares: api-stripprefix
|
||||
traefik.http.services.api.loadbalancer.server.port: 3001
|
||||
# Websecure
|
||||
traefik.http.routers.api-secure.rule: (Host(`${DOMAIN}`) && PathPrefix(`/api`))
|
||||
traefik.http.routers.api-secure.entrypoints: websecure
|
||||
traefik.http.routers.api-secure.service: api-secure
|
||||
traefik.http.routers.api-secure.tls.certresolver: myresolver
|
||||
traefik.http.routers.api-secure.middlewares: api-stripprefix
|
||||
traefik.http.services.api-secure.loadbalancer.server.port: 3001
|
||||
# Middlewares
|
||||
traefik.http.middlewares.api-stripprefix.stripprefix.prefixes: /api
|
||||
|
||||
dashboard:
|
||||
image: meienberger/runtipi:rc-${TIPI_VERSION}
|
||||
command: /bin/sh -c "cd /dashboard && node server.js"
|
||||
container_name: dashboard
|
||||
networks:
|
||||
- tipi_main_network
|
||||
depends_on:
|
||||
api:
|
||||
condition: service_started
|
||||
environment:
|
||||
INTERNAL_IP: ${INTERNAL_IP}
|
||||
NODE_ENV: production
|
||||
DOMAIN: ${DOMAIN}
|
||||
NGINX_PORT: ${NGINX_PORT-80}
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.dashboard-redirect.rule: PathPrefix("/")
|
||||
traefik.http.routers.dashboard-redirect.entrypoints: web
|
||||
traefik.http.routers.dashboard-redirect.middlewares: redirect-middleware
|
||||
traefik.http.routers.dashboard-redirect.service: dashboard
|
||||
traefik.http.services.dashboard-redirect.loadbalancer.server.port: 3000
|
||||
|
||||
traefik.http.routers.dashboard-redirect-secure.rule: Host(`${DOMAIN}`) && PathPrefix(`/`)
|
||||
traefik.http.routers.dashboard-redirect-secure.entrypoints: websecure
|
||||
traefik.http.routers.dashboard-redirect-secure.middlewares: redirect-middleware
|
||||
traefik.http.routers.dashboard-redirect-secure.service: dashboard
|
||||
traefik.http.routers.dashboard-redirect-secure.tls.certresolver: myresolver
|
||||
traefik.http.services.dashboard-redirect-secure.loadbalancer.server.port: 3000
|
||||
|
||||
# Web
|
||||
traefik.http.routers.dashboard.rule: PathPrefix("/dashboard")
|
||||
traefik.http.routers.dashboard.service: dashboard
|
||||
traefik.http.routers.dashboard.entrypoints: web
|
||||
traefik.http.services.dashboard.loadbalancer.server.port: 3000
|
||||
# Websecure
|
||||
traefik.http.routers.dashboard-secure.rule: Host(`${DOMAIN}`) && PathPrefix(`/dashboard`)
|
||||
traefik.http.routers.dashboard-secure.service: dashboard-secure
|
||||
traefik.http.routers.dashboard-secure.entrypoints: websecure
|
||||
traefik.http.routers.dashboard-secure.tls.certresolver: myresolver
|
||||
traefik.http.services.dashboard-secure.loadbalancer.server.port: 3000
|
||||
# Middlewares
|
||||
traefik.http.middlewares.redirect-middleware.redirectregex.regex: .*
|
||||
traefik.http.middlewares.redirect-middleware.redirectregex.replacement: /dashboard
|
||||
|
||||
networks:
|
||||
tipi_main_network:
|
||||
driver: bridge
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: 10.21.21.0/24
|
147
docker-compose.yml
Normal file
147
docker-compose.yml
Normal file
|
@ -0,0 +1,147 @@
|
|||
version: "3.7"
|
||||
|
||||
services:
|
||||
reverse-proxy:
|
||||
container_name: reverse-proxy
|
||||
image: traefik:v2.8
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- ${NGINX_PORT-80}:80
|
||||
- ${NGINX_PORT_SSL-443}:443
|
||||
command: --providers.docker
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ${PWD}/traefik:/root/.config
|
||||
- ${PWD}/traefik/shared:/shared
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
tipi-db:
|
||||
container_name: tipi-db
|
||||
image: postgres:14
|
||||
restart: unless-stopped
|
||||
stop_grace_period: 1m
|
||||
volumes:
|
||||
- ${PWD}/data/postgres:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_USER: tipi
|
||||
POSTGRES_DB: tipi
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -d tipi -U tipi"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 120
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
tipi-redis:
|
||||
container_name: tipi-redis
|
||||
image: redis:alpine
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data/redis:/data
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
api:
|
||||
image: meienberger/runtipi:${TIPI_VERSION}
|
||||
command: /bin/sh -c "cd /api && npm run start"
|
||||
restart: unless-stopped
|
||||
container_name: api
|
||||
depends_on:
|
||||
tipi-db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ${PWD}/repos:/runtipi/repos:ro
|
||||
- ${PWD}/apps:/runtipi/apps
|
||||
- ${PWD}/state:/runtipi/state
|
||||
- ${PWD}/logs:/app/logs
|
||||
- ${STORAGE_PATH}:/app/storage
|
||||
- ${PWD}/.env:/runtipi/.env:ro
|
||||
environment:
|
||||
INTERNAL_IP: ${INTERNAL_IP}
|
||||
TIPI_VERSION: ${TIPI_VERSION}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
NGINX_PORT: ${NGINX_PORT}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_USERNAME: tipi
|
||||
POSTGRES_DBNAME: tipi
|
||||
POSTGRES_HOST: tipi-db
|
||||
NODE_ENV: production
|
||||
APPS_REPO_ID: ${APPS_REPO_ID}
|
||||
APPS_REPO_URL: ${APPS_REPO_URL}
|
||||
DOMAIN: ${DOMAIN}
|
||||
ARCHITECTURE: ${ARCHITECTURE}
|
||||
networks:
|
||||
- tipi_main_network
|
||||
labels:
|
||||
traefik.enable: true
|
||||
# Web
|
||||
traefik.http.routers.api.rule: PathPrefix(`/api`)
|
||||
traefik.http.routers.api.service: api
|
||||
traefik.http.routers.api.entrypoints: web
|
||||
traefik.http.routers.api.middlewares: api-stripprefix
|
||||
traefik.http.services.api.loadbalancer.server.port: 3001
|
||||
# Websecure
|
||||
traefik.http.routers.api-secure.rule: (Host(`${DOMAIN}`) && PathPrefix(`/api`))
|
||||
traefik.http.routers.api-secure.entrypoints: websecure
|
||||
traefik.http.routers.api-secure.service: api-secure
|
||||
traefik.http.routers.api-secure.tls.certresolver: myresolver
|
||||
traefik.http.routers.api-secure.middlewares: api-stripprefix
|
||||
traefik.http.services.api-secure.loadbalancer.server.port: 3001
|
||||
# Middlewares
|
||||
traefik.http.middlewares.api-stripprefix.stripprefix.prefixes: /api
|
||||
|
||||
dashboard:
|
||||
image: meienberger/runtipi:${TIPI_VERSION}
|
||||
command: /bin/sh -c "cd /dashboard && node server.js"
|
||||
restart: unless-stopped
|
||||
container_name: dashboard
|
||||
networks:
|
||||
- tipi_main_network
|
||||
depends_on:
|
||||
api:
|
||||
condition: service_started
|
||||
environment:
|
||||
INTERNAL_IP: ${INTERNAL_IP}
|
||||
NODE_ENV: production
|
||||
DOMAIN: ${DOMAIN}
|
||||
NGINX_PORT: ${NGINX_PORT-80}
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.dashboard-redirect.rule: PathPrefix("/")
|
||||
traefik.http.routers.dashboard-redirect.entrypoints: web
|
||||
traefik.http.routers.dashboard-redirect.middlewares: redirect-middleware
|
||||
traefik.http.routers.dashboard-redirect.service: dashboard
|
||||
traefik.http.services.dashboard-redirect.loadbalancer.server.port: 3000
|
||||
|
||||
traefik.http.routers.dashboard-redirect-secure.rule: Host(`${DOMAIN}`) && PathPrefix(`/`)
|
||||
traefik.http.routers.dashboard-redirect-secure.entrypoints: websecure
|
||||
traefik.http.routers.dashboard-redirect-secure.middlewares: redirect-middleware
|
||||
traefik.http.routers.dashboard-redirect-secure.service: dashboard
|
||||
traefik.http.routers.dashboard-redirect-secure.tls.certresolver: myresolver
|
||||
traefik.http.services.dashboard-redirect-secure.loadbalancer.server.port: 3000
|
||||
|
||||
# Web
|
||||
traefik.http.routers.dashboard.rule: PathPrefix("/dashboard")
|
||||
traefik.http.routers.dashboard.service: dashboard
|
||||
traefik.http.routers.dashboard.entrypoints: web
|
||||
traefik.http.services.dashboard.loadbalancer.server.port: 3000
|
||||
# Websecure
|
||||
traefik.http.routers.dashboard-secure.rule: Host(`${DOMAIN}`) && PathPrefix(`/dashboard`)
|
||||
traefik.http.routers.dashboard-secure.service: dashboard-secure
|
||||
traefik.http.routers.dashboard-secure.entrypoints: websecure
|
||||
traefik.http.routers.dashboard-secure.tls.certresolver: myresolver
|
||||
traefik.http.services.dashboard-secure.loadbalancer.server.port: 3000
|
||||
# Middlewares
|
||||
traefik.http.middlewares.redirect-middleware.redirectregex.regex: .*
|
||||
traefik.http.middlewares.redirect-middleware.redirectregex.replacement: /dashboard
|
||||
|
||||
networks:
|
||||
tipi_main_network:
|
||||
driver: bridge
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: 10.21.21.0/24
|
|
@ -1,29 +0,0 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
import { testUser } from './helpers/constants';
|
||||
import { clearDatabase } from './helpers/db';
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await clearDatabase();
|
||||
});
|
||||
|
||||
test('user should be redirected to /register', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
await page.waitForURL(/register/);
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Register your account' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('user can register a new account', async ({ page }) => {
|
||||
await page.goto('/register');
|
||||
|
||||
await page.getByPlaceholder('you@example.com').click();
|
||||
await page.getByPlaceholder('you@example.com').fill(testUser.email);
|
||||
|
||||
await page.getByPlaceholder('Enter your password', { exact: true }).fill(testUser.password);
|
||||
await page.getByPlaceholder('Confirm your password').fill(testUser.password);
|
||||
|
||||
await page.getByRole('button', { name: 'Register' }).click();
|
||||
await expect(page).toHaveTitle(/Dashboard/);
|
||||
});
|
|
@ -1,26 +0,0 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { loginUser, createTestUser } from './fixtures/fixtures';
|
||||
import { testUser } from './helpers/constants';
|
||||
import { clearDatabase } from './helpers/db';
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await clearDatabase();
|
||||
});
|
||||
|
||||
test('user can login and is redirected to the dashboard', async ({ page }) => {
|
||||
await createTestUser();
|
||||
await page.goto('/login');
|
||||
|
||||
await page.getByPlaceholder('you@example.com').fill(testUser.email);
|
||||
await page.getByPlaceholder('Your password').fill(testUser.password);
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('user can logout', async ({ page }) => {
|
||||
await loginUser(page);
|
||||
await page.getByTestId('logout-button').click();
|
||||
|
||||
await expect(page.getByText('Login to your account')).toBeVisible();
|
||||
});
|
|
@ -1,57 +0,0 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { loginUser } from './fixtures/fixtures';
|
||||
import { clearDatabase } from './helpers/db';
|
||||
|
||||
test.beforeEach(async ({ page, isMobile }) => {
|
||||
await clearDatabase();
|
||||
await loginUser(page);
|
||||
|
||||
if (isMobile) {
|
||||
// TODO: Fix mobile accessibility for the dropdown menu
|
||||
// await page.getByRole('button', { name: 'Menu' }).click();
|
||||
await page.goto('/app-store');
|
||||
} else {
|
||||
await page.getByRole('link', { name: 'App store' }).click();
|
||||
}
|
||||
|
||||
await page.getByPlaceholder('Search').fill('hello');
|
||||
await page.getByRole('link', { name: 'Hello World' }).click();
|
||||
});
|
||||
|
||||
test('user can install and uninstall app', async ({ page, context }) => {
|
||||
// Install app
|
||||
await page.getByRole('button', { name: 'Install' }).click();
|
||||
|
||||
await expect(page.getByText('Install Hello World')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Install' }).click();
|
||||
|
||||
await expect(page.getByText('Installing')).toBeVisible();
|
||||
await expect(page.getByText('Running')).toBeVisible({ timeout: 60000 });
|
||||
await expect(page.getByText('App installed successfully')).toBeVisible();
|
||||
|
||||
await page.getByTestId('app-details').getByRole('button', { name: 'Open' }).press('ArrowDown');
|
||||
const [newPage] = await Promise.all([context.waitForEvent('page'), await page.getByRole('menuitem', { name: `${process.env.SERVER_IP}:8000` }).click()]);
|
||||
|
||||
await newPage.waitForLoadState();
|
||||
await expect(newPage.getByText('Hello World')).toBeVisible();
|
||||
await newPage.close();
|
||||
|
||||
// Stop app
|
||||
await page.getByRole('button', { name: 'Stop' }).click();
|
||||
await expect(page.getByText('Stop Hello World')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Stop' }).click();
|
||||
|
||||
await expect(page.getByText('Stopping')).toBeVisible();
|
||||
await expect(page.getByText('App stopped successfully')).toBeVisible({ timeout: 60000 });
|
||||
|
||||
// Uninstall app
|
||||
await page.getByRole('button', { name: 'Remove' }).click();
|
||||
await expect(page.getByText('Uninstall Hello World?')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Uninstall' }).click();
|
||||
|
||||
await expect(page.getByText('Uninstalling')).toBeVisible();
|
||||
await expect(page.getByText('App uninstalled successfully')).toBeVisible({ timeout: 60000 });
|
||||
});
|
|
@ -1,81 +0,0 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { loginUser } from './fixtures/fixtures';
|
||||
import { clearDatabase } from './helpers/db';
|
||||
import { testUser } from './helpers/constants';
|
||||
import { setSettings } from './helpers/settings';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setSettings({});
|
||||
await clearDatabase();
|
||||
await loginUser(page);
|
||||
|
||||
await page.goto('/settings');
|
||||
});
|
||||
|
||||
test('user can change their password', async ({ page }) => {
|
||||
// Change password
|
||||
await page.getByRole('tab', { name: 'Security' }).click();
|
||||
|
||||
await page.getByPlaceholder('Current password').click();
|
||||
await page.getByPlaceholder('Current password').fill(testUser.password);
|
||||
await page.getByPlaceholder('New password', { exact: true }).click();
|
||||
await page.getByPlaceholder('New password', { exact: true }).fill('password2');
|
||||
await page.getByPlaceholder('Confirm new password').click();
|
||||
await page.getByPlaceholder('Confirm new password').fill('password2');
|
||||
await page.getByRole('button', { name: 'Change password' }).click();
|
||||
|
||||
await expect(page.getByText('Password changed successfully')).toBeVisible();
|
||||
|
||||
// Login with new password
|
||||
await page.getByPlaceholder('you@example.com').fill(testUser.email);
|
||||
await page.getByPlaceholder('Your password').fill('password2');
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('user can change their email', async ({ page }) => {
|
||||
// Change email
|
||||
const newEmail = 'tester2@test.com';
|
||||
|
||||
await page.getByRole('tab', { name: 'Security' }).click();
|
||||
await page.getByRole('button', { name: 'Change username' }).click();
|
||||
await page.getByPlaceholder('New username').click();
|
||||
await page.getByPlaceholder('New username').fill(newEmail);
|
||||
|
||||
// Wrong password
|
||||
await page.getByPlaceholder('Password', { exact: true }).click();
|
||||
await page.getByPlaceholder('Password', { exact: true }).fill('incorrect');
|
||||
|
||||
await page.getByRole('button', { name: 'Change username' }).click();
|
||||
|
||||
await expect(page.getByText('Invalid password')).toBeVisible();
|
||||
|
||||
// Wrong email
|
||||
await page.getByPlaceholder('Password', { exact: true }).click();
|
||||
await page.getByPlaceholder('Password', { exact: true }).fill(testUser.password);
|
||||
await page.getByPlaceholder('New username').click();
|
||||
await page.getByPlaceholder('New username').fill('incorrect');
|
||||
|
||||
await page.getByRole('button', { name: 'Change username' }).click();
|
||||
|
||||
await expect(page.getByText('Must be a valid email address')).toBeVisible();
|
||||
|
||||
// Correct email and password
|
||||
await page.getByPlaceholder('New username').click();
|
||||
await page.getByPlaceholder('New username').fill(newEmail);
|
||||
|
||||
await page.getByRole('button', { name: 'Change username' }).click();
|
||||
|
||||
await expect(page.getByText('Username changed successfully')).toBeVisible();
|
||||
|
||||
// Login with new email
|
||||
await page.getByPlaceholder('you@example.com').click();
|
||||
await page.getByPlaceholder('you@example.com').fill(newEmail);
|
||||
await page.getByPlaceholder('Your password').click();
|
||||
await page.getByPlaceholder('Your password').fill(testUser.password);
|
||||
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
|
||||
});
|
|
@ -1,58 +0,0 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { appTable } from '@/server/db/schema';
|
||||
import { setSettings } from './helpers/settings';
|
||||
import { loginUser } from './fixtures/fixtures';
|
||||
import { clearDatabase, db } from './helpers/db';
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await clearDatabase();
|
||||
await setSettings({});
|
||||
});
|
||||
|
||||
test('user can activate the guest dashboard and see it when logged out', async ({ page }) => {
|
||||
await loginUser(page);
|
||||
await page.goto('/settings');
|
||||
|
||||
await page.getByRole('tab', { name: 'Settings' }).click();
|
||||
await page.getByLabel('guestDashboard').setChecked(true);
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByTestId('logout-button').click();
|
||||
|
||||
await expect(page.getByText('No apps to display')).toBeVisible();
|
||||
});
|
||||
|
||||
test('logged out users can see the apps on the guest dashboard', async ({ browser }) => {
|
||||
await setSettings({ guestDashboard: true });
|
||||
await db.insert(appTable).values({ config: {}, isVisibleOnGuestDashboard: true, id: 'hello-world', exposed: true, domain: 'duckduckgo.com', status: 'running' });
|
||||
await db.insert(appTable).values({ config: {}, isVisibleOnGuestDashboard: false, id: 'actual-budget', exposed: false, status: 'running' });
|
||||
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
await page.goto('/');
|
||||
await expect(page.getByText(/Hello World web server/)).toBeVisible();
|
||||
const locator = page.locator('text=Actual Budget');
|
||||
expect(locator).not.toBeVisible();
|
||||
|
||||
const [newPage] = await Promise.all([context.waitForEvent('page'), await page.getByRole('link', { name: /Hello World/ }).click()]);
|
||||
|
||||
await newPage.waitForLoadState();
|
||||
expect(newPage.url()).toBe('https://duckduckgo.com/');
|
||||
await newPage.close();
|
||||
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('user can deactivate the guest dashboard and not see it when logged out', async ({ page }) => {
|
||||
await loginUser(page);
|
||||
await page.goto('/settings');
|
||||
|
||||
await page.getByRole('tab', { name: 'Settings' }).click();
|
||||
await page.getByLabel('guestDashboard').setChecked(false);
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByTestId('logout-button').click();
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
// We should be redirected to the login page
|
||||
await expect(page.getByRole('heading', { name: 'Login' })).toBeVisible();
|
||||
});
|
|
@ -1,25 +0,0 @@
|
|||
import * as argon2 from 'argon2';
|
||||
import { expect, Page } from '@playwright/test';
|
||||
import { userTable } from '@/server/db/schema';
|
||||
import { db } from '../helpers/db';
|
||||
import { testUser } from '../helpers/constants';
|
||||
|
||||
export const createTestUser = async () => {
|
||||
// Create user in database
|
||||
const password = await argon2.hash(testUser.password);
|
||||
await db.insert(userTable).values({ password, username: testUser.email, operator: true });
|
||||
};
|
||||
|
||||
export const loginUser = async (page: Page) => {
|
||||
// Create user in database
|
||||
await createTestUser();
|
||||
|
||||
// Login flow
|
||||
await page.goto('/login');
|
||||
|
||||
await page.getByPlaceholder('you@example.com').fill(testUser.email);
|
||||
await page.getByPlaceholder('Your password').fill(testUser.password);
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
export const testUser = {
|
||||
email: 'tester@test.com',
|
||||
password: 'password',
|
||||
};
|
|
@ -1,17 +0,0 @@
|
|||
import { Pool } from 'pg';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import * as schema from '../../src/server/db/schema';
|
||||
|
||||
const connectionString = `postgresql://tipi:${process.env.POSTGRES_PASSWORD}@${process.env.SERVER_IP}:5432/tipi?connect_timeout=300`;
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString,
|
||||
});
|
||||
|
||||
export const db = drizzle(pool, { schema });
|
||||
|
||||
export const clearDatabase = async () => {
|
||||
// delete all data in table user
|
||||
await db.delete(schema.userTable);
|
||||
await db.delete(schema.appTable);
|
||||
};
|
|
@ -1,12 +0,0 @@
|
|||
import { clearDatabase } from './db';
|
||||
import { setSettings } from './settings';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
async function globalSetup() {
|
||||
await clearDatabase();
|
||||
await setSettings({});
|
||||
}
|
||||
|
||||
export default globalSetup;
|
|
@ -1,8 +0,0 @@
|
|||
import { promises } from 'fs';
|
||||
import path from 'path';
|
||||
import { z } from 'zod';
|
||||
import { settingsSchema } from '@runtipi/shared';
|
||||
|
||||
export const setSettings = async (settings: z.infer<typeof settingsSchema>) => {
|
||||
await promises.writeFile(path.join(__dirname, '../../state/settings.json'), JSON.stringify(settings));
|
||||
};
|
2
global.d.ts
vendored
2
global.d.ts
vendored
|
@ -1,2 +0,0 @@
|
|||
type Messages = typeof import('./src/client/messages/en.json');
|
||||
type IntlMessages = Messages;
|
7
jest.config.js
Normal file
7
jest.config.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
testMatch: ["**/__tests__/**/*.test.ts"],
|
||||
testPathIgnorePatterns: ["/node_modules/", "/packages/"],
|
||||
};
|
|
@ -1,40 +0,0 @@
|
|||
import nextJest from 'next/jest';
|
||||
|
||||
const createJestConfig = nextJest({
|
||||
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
|
||||
dir: './',
|
||||
});
|
||||
|
||||
const customClientConfig = {
|
||||
testEnvironment: 'jest-environment-jsdom',
|
||||
setupFilesAfterEnv: ['<rootDir>/tests/client/jest.setup.tsx'],
|
||||
testMatch: ['<rootDir>/src/client/**/*.{spec,test}.{ts,tsx}', '!<rootDir>/src/server/**/*.{spec,test}.{ts,tsx}'],
|
||||
};
|
||||
|
||||
const customServerConfig = {
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['<rootDir>/src/server/**/*.test.ts'],
|
||||
setupFilesAfterEnv: ['<rootDir>/tests/server/jest.setup.ts'],
|
||||
};
|
||||
|
||||
export default async () => {
|
||||
const clientConfig = await createJestConfig(customClientConfig)();
|
||||
const serverConfig = await createJestConfig(customServerConfig)();
|
||||
|
||||
return {
|
||||
randomize: true,
|
||||
verbose: true,
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ['src/server/**/*.{ts,tsx}', 'src/client/**/*.{ts,tsx}', '!src/**/mocks/**/*.{ts,tsx}', '!**/*.{spec,test}.{ts,tsx}', '!**/index.{ts,tsx}'],
|
||||
projects: [
|
||||
{
|
||||
displayName: 'client',
|
||||
...clientConfig,
|
||||
},
|
||||
{
|
||||
displayName: 'server',
|
||||
...serverConfig,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
0
media/data/.gitkeep
Normal file
0
media/data/.gitkeep
Normal file
0
media/data/books/.gitkeep
Normal file
0
media/data/books/.gitkeep
Normal file
0
media/data/books/ebooks/.gitkeep
Normal file
0
media/data/books/ebooks/.gitkeep
Normal file
BIN
media/data/books/metadata.db
Normal file
BIN
media/data/books/metadata.db
Normal file
Binary file not shown.
0
media/data/books/spoken/.gitkeep
Normal file
0
media/data/books/spoken/.gitkeep
Normal file
0
media/data/images/.gitkeep
Normal file
0
media/data/images/.gitkeep
Normal file
0
media/data/movies/.gitkeep
Normal file
0
media/data/movies/.gitkeep
Normal file
0
media/data/music/.gitkeep
Normal file
0
media/data/music/.gitkeep
Normal file
0
media/data/podcasts/.gitkeep
Normal file
0
media/data/podcasts/.gitkeep
Normal file
0
media/data/tv/.gitkeep
Normal file
0
media/data/tv/.gitkeep
Normal file
0
media/torrents/.gitkeep
Normal file
0
media/torrents/.gitkeep
Normal file
0
media/torrents/complete/.gitkeep
Normal file
0
media/torrents/complete/.gitkeep
Normal file
0
media/torrents/incomplete/.gitkeep
Normal file
0
media/torrents/incomplete/.gitkeep
Normal file
0
media/torrents/watch/.gitkeep
Normal file
0
media/torrents/watch/.gitkeep
Normal file
|
@ -1,35 +0,0 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
swcMinify: true,
|
||||
output: 'standalone',
|
||||
reactStrictMode: true,
|
||||
transpilePackages: ['@runtipi/shared'],
|
||||
experimental: {
|
||||
serverComponentsExternalPackages: ['bullmq'],
|
||||
},
|
||||
serverRuntimeConfig: {
|
||||
INTERNAL_IP: process.env.INTERNAL_IP,
|
||||
TIPI_VERSION: process.env.TIPI_VERSION,
|
||||
JWT_SECRET: process.env.JWT_SECRET,
|
||||
POSTGRES_PASSWORD: process.env.POSTGRES_PASSWORD,
|
||||
POSTGRES_USERNAME: process.env.POSTGRES_USERNAME,
|
||||
POSTGRES_DBNAME: process.env.POSTGRES_DBNAME,
|
||||
POSTGRES_HOST: process.env.POSTGRES_HOST,
|
||||
APPS_REPO_ID: process.env.APPS_REPO_ID,
|
||||
APPS_REPO_URL: process.env.APPS_REPO_URL,
|
||||
DOMAIN: process.env.DOMAIN,
|
||||
ARCHITECTURE: process.env.ARCHITECTURE,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
REDIS_HOST: process.env.REDIS_HOST,
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/apps/:id',
|
||||
destination: '/app-store/:id',
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
162
package.json
162
package.json
|
@ -1,156 +1,40 @@
|
|||
{
|
||||
"name": "runtipi",
|
||||
"version": "2.2.1",
|
||||
"version": "0.7.2",
|
||||
"description": "A homeserver for everyone",
|
||||
"scripts": {
|
||||
"knip": "knip",
|
||||
"prepare": "mkdir -p state && echo \"{}\" > state/system-info.json && echo \"random-seed\" > state/seed",
|
||||
"test": "dotenv -e .env.test -- jest --colors",
|
||||
"test:e2e": "NODE_ENV=test dotenv -e .env -e .env.e2e -- playwright test",
|
||||
"test:e2e:ui": "NODE_ENV=test dotenv -e .env -e .env.e2e -- playwright test --ui",
|
||||
"test:client": "jest --colors --selectProjects client --",
|
||||
"test:server": "jest --colors --selectProjects server --",
|
||||
"test:vite": "dotenv -e .env.test -- vitest run --coverage",
|
||||
"dev": "next dev",
|
||||
"dev:watcher": "pnpm -r --filter cli dev",
|
||||
"db:migrate": "NODE_ENV=development dotenv -e .env.local -- tsx ./src/server/run-migrations-dev.ts",
|
||||
"lint": "next lint",
|
||||
"lint:fix": "next lint --fix",
|
||||
"build": "next build",
|
||||
"start": "NODE_ENV=production node server.js",
|
||||
"start:dev-container": "./.devcontainer/filewatcher.sh && npm run start:dev",
|
||||
"start:rc": "docker compose -f docker-compose.rc.yml --env-file .env up --build",
|
||||
"start:dev": "npm run prepare && docker compose -f docker-compose.dev.yml up --build",
|
||||
"start:prod": "npm run prepare && docker compose --env-file ./.env -f docker-compose.prod.yml up --build",
|
||||
"start:pg": "docker run --name test-db -p 5433:5432 -d --rm -e POSTGRES_PASSWORD=postgres postgres:14",
|
||||
"prepare": "husky install",
|
||||
"commit": "git-cz",
|
||||
"act:test-install": "act --container-architecture linux/amd64 -j test-install",
|
||||
"act:docker": "act --container-architecture linux/amd64 --secret-file github.secrets -j build-images",
|
||||
"start:dev": "./scripts/start-dev.sh",
|
||||
"start:rc": "docker-compose -f docker-compose.rc.yml --env-file .env up --build",
|
||||
"start:prod": "docker-compose --env-file .env up --build",
|
||||
"start:pg": "docker run --name test-db -p 5433:5432 -d --rm -e POSTGRES_PASSWORD=postgres postgres",
|
||||
"version": "echo $npm_package_version",
|
||||
"release:rc": "./scripts/deploy/release-rc.sh",
|
||||
"test:build": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t meienberger/runtipi:test .",
|
||||
"test:build:arm64": "docker buildx build --platform linux/arm64 -t meienberger/runtipi:test .",
|
||||
"test:build:arm7": "docker buildx build --platform linux/arm/v7 -t meienberger/runtipi:test .",
|
||||
"test:build:amd64": "docker buildx build --platform linux/amd64 -t meienberger/runtipi:test .",
|
||||
"tsc": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.3.2",
|
||||
"@otplib/core": "^12.0.1",
|
||||
"@otplib/plugin-crypto": "^12.0.1",
|
||||
"@otplib/plugin-thirty-two": "^12.0.1",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@runtipi/postgres-migrations": "^5.3.0",
|
||||
"@runtipi/shared": "workspace:^",
|
||||
"@tabler/core": "1.0.0-beta20",
|
||||
"@tabler/icons-react": "^2.42.0",
|
||||
"argon2": "^0.31.2",
|
||||
"bullmq": "^4.13.0",
|
||||
"clsx": "^2.0.0",
|
||||
"connect-redis": "^7.1.0",
|
||||
"drizzle-orm": "^0.28.6",
|
||||
"fs-extra": "^11.1.1",
|
||||
"geist": "^1.2.0",
|
||||
"let-it-go": "^1.0.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"next": "14.0.1",
|
||||
"next-client-cookies": "^1.0.6",
|
||||
"next-intl": "^2.22.1",
|
||||
"next-safe-action": "^5.0.2",
|
||||
"pg": "^8.11.3",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.48.2",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-markdown": "^9.0.0",
|
||||
"react-select": "^5.8.0",
|
||||
"react-tooltip": "^5.25.0",
|
||||
"redaxios": "^0.5.1",
|
||||
"redis": "^4.6.10",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"sass": "^1.69.5",
|
||||
"semver": "^7.5.4",
|
||||
"sharp": "0.32.6",
|
||||
"swr": "^2.2.4",
|
||||
"tslib": "^2.6.2",
|
||||
"uuid": "^9.0.1",
|
||||
"validator": "^13.11.0",
|
||||
"winston": "^3.11.0",
|
||||
"zod": "^3.22.4",
|
||||
"zustand": "^4.4.6"
|
||||
"release:rc": "./scripts/deploy/release-rc.sh"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.23.2",
|
||||
"@faker-js/faker": "^8.2.0",
|
||||
"@playwright/test": "^1.39.0",
|
||||
"@testing-library/dom": "^9.3.3",
|
||||
"@testing-library/jest-dom": "^6.1.4",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^14.5.1",
|
||||
"@total-typescript/shoehorn": "^0.1.1",
|
||||
"@total-typescript/ts-reset": "^0.5.1",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/lodash.merge": "^4.6.8",
|
||||
"@types/node": "20.8.10",
|
||||
"@types/pg": "^8.10.7",
|
||||
"@types/react": "18.2.39",
|
||||
"@types/react-dom": "18.2.14",
|
||||
"@types/semver": "^7.5.4",
|
||||
"@types/uuid": "^9.0.6",
|
||||
"@types/validator": "^13.11.5",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.1",
|
||||
"@typescript-eslint/parser": "^6.10.0",
|
||||
"@vitejs/plugin-react": "^4.1.1",
|
||||
"@vitest/coverage-v8": "^0.34.6",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"eslint": "8.52.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.1.0",
|
||||
"eslint-config-next": "14.0.3",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"eslint-plugin-jest": "^27.6.0",
|
||||
"eslint-plugin-jest-dom": "^5.1.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-testing-library": "^6.1.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"knip": "^2.41.3",
|
||||
"memfs": "^4.6.0",
|
||||
"msw": "^1.3.2",
|
||||
"next-router-mock": "^0.9.10",
|
||||
"prettier": "^3.0.3",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsx": "^3.14.0",
|
||||
"typescript": "5.2.2",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
"vitest": "^0.34.6",
|
||||
"wait-for-expect": "^3.0.2"
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": "public"
|
||||
"@commitlint/cli": "^17.0.3",
|
||||
"@commitlint/config-conventional": "^17.0.3",
|
||||
"@commitlint/cz-commitlint": "^17.0.3",
|
||||
"commitizen": "^4.2.4",
|
||||
"husky": "^8.0.1",
|
||||
"inquirer": "8.2.4"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/runtipi/runtipi.git"
|
||||
"url": "git+https://github.com/meienberger/runtipi.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "GNU General Public License v3.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/runtipi/runtipi/issues"
|
||||
"url": "https://github.com/meienberger/runtipi/issues"
|
||||
},
|
||||
"homepage": "https://github.com/runtipi/runtipi#readme",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {}
|
||||
"homepage": "https://github.com/meienberger/runtipi#readme",
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "@commitlint/cz-commitlint"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
INTERNAL_IP=localhost
|
||||
ARCHITECTURE=arm64
|
||||
APPS_REPO_ID=repo-id
|
||||
APPS_REPO_URL=https://test.com/test
|
||||
ROOT_FOLDER_HOST=/runtipi
|
||||
STORAGE_PATH=/runtipi
|
||||
TIPI_VERSION=1
|
||||
REDIS_PASSWORD=redis
|
||||
POSTGRES_HOST=localhost
|
||||
POSTGRES_DBNAME=postgres
|
||||
POSTGRES_USERNAME=postgres
|
||||
POSTGRES_PASSWORD=postgres
|
||||
POSTGRES_PORT=5433
|
|
@ -1 +0,0 @@
|
|||
.eslintrc.js
|
|
@ -1,39 +0,0 @@
|
|||
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
packages/cli/.gitignore
vendored
4
packages/cli/.gitignore
vendored
|
@ -1,4 +0,0 @@
|
|||
dev
|
||||
dist/
|
||||
coverage/
|
||||
assets/VERSION
|
|
@ -1,150 +0,0 @@
|
|||
version: '3.7'
|
||||
|
||||
services:
|
||||
tipi-reverse-proxy:
|
||||
container_name: tipi-reverse-proxy
|
||||
image: traefik:v2.8
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- tipi-dashboard
|
||||
ports:
|
||||
- ${NGINX_PORT:-80}:80
|
||||
- ${NGINX_PORT_SSL:-443}:443
|
||||
command: --providers.docker
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ./traefik:/root/.config
|
||||
- ./traefik/shared:/shared
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
tipi-db:
|
||||
container_name: tipi-db
|
||||
image: postgres:14
|
||||
restart: unless-stopped
|
||||
stop_grace_period: 1m
|
||||
ports:
|
||||
- ${POSTGRES_PORT:-5432}:5432
|
||||
volumes:
|
||||
- ./data/postgres:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_USER: tipi
|
||||
POSTGRES_DB: tipi
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -d tipi -U tipi']
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 120
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
tipi-redis:
|
||||
container_name: tipi-redis
|
||||
image: redis:7.2.0
|
||||
restart: unless-stopped
|
||||
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||
ports:
|
||||
- 6379:6379
|
||||
volumes:
|
||||
- ./data/redis:/data
|
||||
healthcheck:
|
||||
test: ['CMD', 'redis-cli', 'ping']
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 120
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
tipi-worker:
|
||||
container_name: tipi-worker
|
||||
image: ghcr.io/runtipi/worker:${TIPI_VERSION}
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '-f', 'http://localhost:3000/healthcheck']
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 120
|
||||
start_period: 5s
|
||||
depends_on:
|
||||
tipi-db:
|
||||
condition: service_healthy
|
||||
tipi-redis:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
volumes:
|
||||
# Core
|
||||
- /proc:/host/proc
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# App
|
||||
- ./.env:/app/.env
|
||||
- ./state:/app/state
|
||||
- ./repos:/app/repos
|
||||
- ./apps:/app/apps
|
||||
- ./logs:/app/logs
|
||||
- ./traefik:/app/traefik
|
||||
- ./user-config:/app/user-config
|
||||
- ./media:/app/media
|
||||
- ${STORAGE_PATH:-.}:/storage
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
tipi-dashboard:
|
||||
image: ghcr.io/runtipi/runtipi:${TIPI_VERSION}
|
||||
restart: unless-stopped
|
||||
container_name: tipi-dashboard
|
||||
networks:
|
||||
- tipi_main_network
|
||||
depends_on:
|
||||
tipi-db:
|
||||
condition: service_healthy
|
||||
tipi-redis:
|
||||
condition: service_healthy
|
||||
tipi-worker:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./.env:/runtipi/.env:ro
|
||||
- ./state:/runtipi/state
|
||||
- ./repos:/runtipi/repos:ro
|
||||
- ./apps:/runtipi/apps
|
||||
- ./logs:/app/logs
|
||||
- ${STORAGE_PATH:-.}:/app/storage
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
labels:
|
||||
# Main
|
||||
traefik.enable: true
|
||||
traefik.http.middlewares.redirect-to-https.redirectscheme.scheme: https
|
||||
traefik.http.services.dashboard.loadbalancer.server.port: 3000
|
||||
# Local ip
|
||||
traefik.http.routers.dashboard.rule: PathPrefix("/")
|
||||
traefik.http.routers.dashboard.service: dashboard
|
||||
traefik.http.routers.dashboard.entrypoints: web
|
||||
# Websecure
|
||||
traefik.http.routers.dashboard-insecure.rule: Host(`${DOMAIN}`) && PathPrefix(`/`)
|
||||
traefik.http.routers.dashboard-insecure.service: dashboard
|
||||
traefik.http.routers.dashboard-insecure.entrypoints: web
|
||||
traefik.http.routers.dashboard-insecure.middlewares: redirect-to-https
|
||||
traefik.http.routers.dashboard-secure.rule: Host(`${DOMAIN}`) && PathPrefix(`/`)
|
||||
traefik.http.routers.dashboard-secure.service: dashboard
|
||||
traefik.http.routers.dashboard-secure.entrypoints: websecure
|
||||
traefik.http.routers.dashboard-secure.tls.certresolver: myresolver
|
||||
# Local domain
|
||||
traefik.http.routers.dashboard-local-insecure.rule: Host(`${LOCAL_DOMAIN}`)
|
||||
traefik.http.routers.dashboard-local-insecure.entrypoints: web
|
||||
traefik.http.routers.dashboard-local-insecure.service: dashboard
|
||||
traefik.http.routers.dashboard-local-insecure.middlewares: redirect-to-https
|
||||
traefik.http.routers.dashboard-local.rule: Host(`${LOCAL_DOMAIN}`)
|
||||
traefik.http.routers.dashboard-local.entrypoints: websecure
|
||||
traefik.http.routers.dashboard-local.tls: true
|
||||
traefik.http.routers.dashboard-local.service: dashboard
|
||||
|
||||
networks:
|
||||
tipi_main_network:
|
||||
driver: bridge
|
||||
name: runtipi_tipi_main_network
|
|
@ -1,21 +0,0 @@
|
|||
const { build } = require('esbuild');
|
||||
|
||||
const commandArgs = process.argv.slice(2);
|
||||
|
||||
async function bundle() {
|
||||
const start = Date.now();
|
||||
const options = {
|
||||
entryPoints: ['./src/index.ts'],
|
||||
outfile: './dist/index.js',
|
||||
platform: 'node',
|
||||
target: 'node20',
|
||||
bundle: true,
|
||||
color: true,
|
||||
sourcemap: commandArgs.includes('--sourcemap'),
|
||||
};
|
||||
|
||||
await build({ ...options, minify: true });
|
||||
console.log(`Build time: ${Date.now() - start}ms`);
|
||||
}
|
||||
|
||||
bundle();
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"watch": ["src"],
|
||||
"exec": "NODE_ENV=development npx tsx ./src/index.ts watch",
|
||||
"ext": "js ts"
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
{
|
||||
"name": "@runtipi/cli",
|
||||
"version": "2.1.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"bin": "dist/index.js",
|
||||
"scripts": {
|
||||
"test": "dotenv -e .env.test vitest -- --coverage --watch=false --passWithNoTests",
|
||||
"test:watch": "dotenv -e .env.test vitest",
|
||||
"package": "npm run build && pkg package.json && chmod +x dist/bin/cli-x64 && chmod +x dist/bin/cli-arm64",
|
||||
"package:m1": "npm run build && pkg package.json -t node20-darwin-arm64",
|
||||
"set-version": "node -e \"require('fs').writeFileSync('assets/VERSION', process.argv[1])\"",
|
||||
"build": "node build.js",
|
||||
"build:meta": "esbuild ./src/index.ts --bundle --platform=node --target=node20 --outfile=dist/index.js --metafile=meta.json --analyze",
|
||||
"dev": "dotenv -e ../../.env nodemon",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"tsc": "tsc --noEmit",
|
||||
"knip": "knip"
|
||||
},
|
||||
"pkg": {
|
||||
"assets": "assets/**/*",
|
||||
"targets": [
|
||||
"node20-linux-x64",
|
||||
"node20-linux-arm64"
|
||||
],
|
||||
"outputPath": "dist/bin"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.2.0",
|
||||
"@types/cli-progress": "^3.11.5",
|
||||
"@types/node": "20.8.10",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"esbuild": "^0.19.4",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"knip": "^2.41.3",
|
||||
"memfs": "^4.6.0",
|
||||
"nodemon": "^3.0.1",
|
||||
"pkg": "^5.8.1",
|
||||
"vite": "^4.5.0",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
"vitest": "^0.34.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@runtipi/shared": "workspace:^",
|
||||
"axios": "^1.6.0",
|
||||
"boxen": "^7.1.1",
|
||||
"bullmq": "^4.13.0",
|
||||
"chalk": "^5.3.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
"cli-spinners": "^2.9.1",
|
||||
"commander": "^11.1.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"log-update": "^5.0.1",
|
||||
"semver": "^7.5.4",
|
||||
"zod": "^3.22.4"
|
||||
}
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
import { Queue, QueueEvents } from 'bullmq';
|
||||
import { SystemEvent, eventSchema } from '@runtipi/shared';
|
||||
import { getEnv } from '@/utils/environment/environment';
|
||||
import { logger } from '@/utils/logger/logger';
|
||||
import { TerminalSpinner } from '@/utils/logger/terminal-spinner';
|
||||
|
||||
export class AppExecutors {
|
||||
private readonly logger;
|
||||
|
||||
constructor() {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
private getQueue = () => {
|
||||
const { redisPassword } = getEnv();
|
||||
const queue = new Queue('events', { connection: { host: '127.0.0.1', port: 6379, password: redisPassword } });
|
||||
const queueEvents = new QueueEvents('events', { connection: { host: '127.0.0.1', port: 6379, password: redisPassword } });
|
||||
|
||||
return { queue, queueEvents };
|
||||
};
|
||||
|
||||
private generateJobId = (event: Record<string, unknown>) => {
|
||||
const { appId, action } = event;
|
||||
return `${appId}-${action}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Stops an app
|
||||
* @param {string} appId - The id of the app to stop
|
||||
*/
|
||||
public stopApp = async (appId: string) => {
|
||||
const spinner = new TerminalSpinner(`Stopping app ${appId}`);
|
||||
spinner.start();
|
||||
|
||||
const jobid = this.generateJobId({ appId, action: 'stop' });
|
||||
|
||||
const { queue, queueEvents } = this.getQueue();
|
||||
const event = { type: 'app', command: 'stop', appid: appId, form: {}, skipEnv: true } satisfies SystemEvent;
|
||||
const job = await queue.add(jobid, eventSchema.parse(event));
|
||||
const result = await job.waitUntilFinished(queueEvents, 1000 * 60 * 5);
|
||||
|
||||
await queueEvents.close();
|
||||
await queue.close();
|
||||
|
||||
if (!result?.success) {
|
||||
this.logger.error(result?.message);
|
||||
spinner.fail(`Failed to stop app ${appId} see logs for more details (logs/error.log)`);
|
||||
} else {
|
||||
spinner.done(`App ${appId} stopped`);
|
||||
}
|
||||
};
|
||||
|
||||
public startApp = async (appId: string) => {
|
||||
const spinner = new TerminalSpinner(`Starting app ${appId}`);
|
||||
spinner.start();
|
||||
|
||||
const jobid = this.generateJobId({ appId, action: 'start' });
|
||||
|
||||
const { queue, queueEvents } = this.getQueue();
|
||||
const event = { type: 'app', command: 'start', appid: appId, form: {}, skipEnv: true } satisfies SystemEvent;
|
||||
const job = await queue.add(jobid, eventSchema.parse(event));
|
||||
const result = await job.waitUntilFinished(queueEvents, 1000 * 60 * 5);
|
||||
|
||||
await queueEvents.close();
|
||||
await queue.close();
|
||||
|
||||
if (!result.success) {
|
||||
spinner.fail(`Failed to start app ${appId} see logs for more details (logs/error.log)`);
|
||||
} else {
|
||||
spinner.done(`App ${appId} started`);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
export { AppExecutors } from './app/app.executors';
|
||||
export { SystemExecutors } from './system/system.executors';
|
|
@ -1,322 +0,0 @@
|
|||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import fs from 'fs';
|
||||
import cliProgress from 'cli-progress';
|
||||
import semver from 'semver';
|
||||
import axios from 'axios';
|
||||
import boxen from 'boxen';
|
||||
import path from 'path';
|
||||
import { spawn } from 'child_process';
|
||||
import { Stream } from 'stream';
|
||||
import dotenv from 'dotenv';
|
||||
import { pathExists } from '@runtipi/shared';
|
||||
import { AppExecutors } from '../app/app.executors';
|
||||
import { copySystemFiles, generateSystemEnvFile } from './system.helpers';
|
||||
import { TerminalSpinner } from '@/utils/logger/terminal-spinner';
|
||||
import { getEnv } from '@/utils/environment/environment';
|
||||
import { logger } from '@/utils/logger/logger';
|
||||
import { execAsync } from '@/utils/exec-async/execAsync';
|
||||
|
||||
export class SystemExecutors {
|
||||
private readonly rootFolder: string;
|
||||
|
||||
private readonly envFile: string;
|
||||
|
||||
private readonly logger;
|
||||
|
||||
constructor() {
|
||||
this.rootFolder = process.cwd();
|
||||
this.logger = logger;
|
||||
|
||||
this.envFile = path.join(this.rootFolder, '.env');
|
||||
}
|
||||
|
||||
private handleSystemError = (err: unknown) => {
|
||||
if (err instanceof Error) {
|
||||
this.logger.error(`An error occurred: ${err.message}`);
|
||||
return { success: false, message: err.message };
|
||||
}
|
||||
this.logger.error(`An error occurred: ${err}`);
|
||||
|
||||
return { success: false, message: `An error occurred: ${err}` };
|
||||
};
|
||||
|
||||
public cleanLogs = async () => {
|
||||
try {
|
||||
await this.logger.flush();
|
||||
this.logger.info('Logs cleaned successfully');
|
||||
|
||||
return { success: true, message: '' };
|
||||
} catch (e) {
|
||||
return this.handleSystemError(e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This method will stop Tipi
|
||||
* It will stop all the apps and then stop the main containers.
|
||||
*/
|
||||
public stop = async () => {
|
||||
const spinner = new TerminalSpinner('Stopping Tipi...');
|
||||
|
||||
try {
|
||||
if (await pathExists(path.join(this.rootFolder, 'apps'))) {
|
||||
const apps = await fs.promises.readdir(path.join(this.rootFolder, 'apps'));
|
||||
const appExecutor = new AppExecutors();
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const app of apps) {
|
||||
spinner.setMessage(`Stopping ${app}...`);
|
||||
spinner.start();
|
||||
await appExecutor.stopApp(app);
|
||||
spinner.done(`${app} stopped`);
|
||||
}
|
||||
}
|
||||
|
||||
spinner.setMessage('Stopping containers...');
|
||||
spinner.start();
|
||||
|
||||
this.logger.info('Stopping main containers...');
|
||||
await execAsync('docker compose down --remove-orphans --rmi local');
|
||||
|
||||
spinner.done('Tipi successfully stopped');
|
||||
|
||||
return { success: true, message: 'Tipi stopped' };
|
||||
} catch (e) {
|
||||
spinner.fail('Tipi failed to stop. Please check the logs for more details (logs/error.log)');
|
||||
return this.handleSystemError(e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This method will start Tipi.
|
||||
* It will copy the system files, generate the system env file, pull the images and start the containers.
|
||||
*/
|
||||
public start = async () => {
|
||||
const spinner = new TerminalSpinner('Starting Tipi...');
|
||||
try {
|
||||
await this.logger.flush();
|
||||
|
||||
// Check if user is in docker group
|
||||
spinner.setMessage('Checking docker permissions...');
|
||||
spinner.start();
|
||||
const { stdout: dockerVersion } = await execAsync('docker --version');
|
||||
|
||||
if (!dockerVersion) {
|
||||
spinner.fail('Your user is not allowed to run docker commands. Please add your user to the docker group or run Tipi as root.');
|
||||
return { success: false, message: 'You need to be in the docker group to run Tipi' };
|
||||
}
|
||||
spinner.done('User allowed to run docker commands');
|
||||
|
||||
spinner.setMessage('Copying system files...');
|
||||
spinner.start();
|
||||
|
||||
this.logger.info('Copying system files...');
|
||||
await copySystemFiles();
|
||||
|
||||
spinner.done('System files copied');
|
||||
|
||||
spinner.setMessage('Generating system env file...');
|
||||
spinner.start();
|
||||
this.logger.info('Generating system env file...');
|
||||
const envMap = await generateSystemEnvFile();
|
||||
spinner.done('System env file generated');
|
||||
|
||||
// Reload env variables after generating the env file
|
||||
this.logger.info('Reloading env variables...');
|
||||
dotenv.config({ path: this.envFile, override: true });
|
||||
|
||||
// Pull images
|
||||
spinner.setMessage('Pulling images...');
|
||||
spinner.start();
|
||||
this.logger.info('Pulling new images...');
|
||||
await execAsync(`docker compose --env-file ${this.envFile} pull`);
|
||||
|
||||
spinner.done('Images pulled');
|
||||
|
||||
// Start containers
|
||||
spinner.setMessage('Starting containers...');
|
||||
spinner.start();
|
||||
this.logger.info('Starting containers...');
|
||||
|
||||
await execAsync(`docker compose --env-file ${this.envFile} up --detach --remove-orphans --build`);
|
||||
spinner.done('Containers started');
|
||||
|
||||
console.log(
|
||||
boxen(
|
||||
`Visit: http://${envMap.get('INTERNAL_IP')}:${envMap.get(
|
||||
'NGINX_PORT',
|
||||
)} to access the dashboard\n\nFind documentation and guides at: https://runtipi.io\n\nTipi is entierly written in TypeScript and we are looking for contributors!`,
|
||||
{
|
||||
title: 'Tipi successfully started 🎉',
|
||||
titleAlignment: 'center',
|
||||
textAlignment: 'center',
|
||||
padding: 1,
|
||||
borderStyle: 'double',
|
||||
borderColor: 'green',
|
||||
width: 80,
|
||||
margin: { top: 1 },
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return { success: true, message: 'Tipi started' };
|
||||
} catch (e) {
|
||||
spinner.fail('Tipi failed to start. Please check the logs for more details (logs/error.log)');
|
||||
return this.handleSystemError(e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This method will stop and start Tipi.
|
||||
*/
|
||||
public restart = async () => {
|
||||
try {
|
||||
await this.stop();
|
||||
await this.start();
|
||||
return { success: true, message: '' };
|
||||
} catch (e) {
|
||||
return this.handleSystemError(e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This method will create a password change request file in the state folder.
|
||||
*/
|
||||
public resetPassword = async () => {
|
||||
try {
|
||||
const { rootFolderHost } = getEnv();
|
||||
await fs.promises.writeFile(path.join(rootFolderHost, 'state', 'password-change-request'), '');
|
||||
return { success: true, message: '' };
|
||||
} catch (e) {
|
||||
return this.handleSystemError(e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a target version, this method will download the corresponding release from GitHub and replace the current
|
||||
* runtipi-cli binary with the new one.
|
||||
* @param {string} target
|
||||
*/
|
||||
public update = async (target: string) => {
|
||||
const spinner = new TerminalSpinner('Evaluating target version...');
|
||||
try {
|
||||
spinner.start();
|
||||
let targetVersion = target;
|
||||
this.logger.info(`Updating Tipi to version ${targetVersion}`);
|
||||
|
||||
if (!targetVersion || targetVersion === 'latest') {
|
||||
spinner.setMessage('Fetching latest version...');
|
||||
const { data } = await axios.get<{ tag_name: string }>('https://api.github.com/repos/runtipi/runtipi/releases/latest');
|
||||
this.logger.info(`Getting latest version from GitHub: ${data.tag_name}`);
|
||||
targetVersion = data.tag_name;
|
||||
}
|
||||
|
||||
if (!semver.valid(targetVersion)) {
|
||||
this.logger.error(`Invalid version: ${targetVersion}`);
|
||||
spinner.fail(`Invalid version: ${targetVersion}`);
|
||||
throw new Error(`Invalid version: ${targetVersion}`);
|
||||
}
|
||||
|
||||
const { rootFolderHost, arch } = getEnv();
|
||||
|
||||
let assetName = 'runtipi-cli-linux-x64';
|
||||
if (arch === 'arm64') {
|
||||
assetName = 'runtipi-cli-linux-arm64';
|
||||
}
|
||||
|
||||
const fileName = `runtipi-cli-${targetVersion}`;
|
||||
const savePath = path.join(rootFolderHost, fileName);
|
||||
const fileUrl = `https://github.com/runtipi/runtipi/releases/download/${targetVersion}/${assetName}`;
|
||||
this.logger.info(`Downloading Tipi ${targetVersion} from ${fileUrl}`);
|
||||
|
||||
spinner.done(`Target version: ${targetVersion}`);
|
||||
spinner.done(`Download url: ${fileUrl}`);
|
||||
|
||||
await this.stop();
|
||||
|
||||
this.logger.info(`Downloading Tipi ${targetVersion}...`);
|
||||
|
||||
const bar = new cliProgress.SingleBar({}, cliProgress.Presets.rect);
|
||||
bar.start(100, 0);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
axios<Stream>({
|
||||
method: 'GET',
|
||||
url: fileUrl,
|
||||
responseType: 'stream',
|
||||
onDownloadProgress: (progress) => {
|
||||
this.logger.info(`Download progress: ${Math.round((progress.loaded / (progress.total || 0)) * 100)}%`);
|
||||
bar.update(Math.round((progress.loaded / (progress.total || 0)) * 100));
|
||||
},
|
||||
}).then((response) => {
|
||||
const writer = fs.createWriteStream(savePath);
|
||||
response.data.pipe(writer);
|
||||
|
||||
writer.on('error', (err) => {
|
||||
bar.stop();
|
||||
this.logger.error(`Failed to download Tipi: ${err}`);
|
||||
spinner.fail(`\nFailed to download Tipi ${targetVersion}`);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
writer.on('finish', () => {
|
||||
this.logger.info('Download complete');
|
||||
bar.stop();
|
||||
resolve('');
|
||||
});
|
||||
});
|
||||
}).catch((e) => {
|
||||
this.logger.error(`Failed to download Tipi: ${e}`);
|
||||
spinner.fail(`\nFailed to download Tipi ${targetVersion}. Please make sure this version exists on GitHub.`);
|
||||
throw e;
|
||||
});
|
||||
|
||||
spinner.done(`Tipi ${targetVersion} downloaded`);
|
||||
this.logger.info(`Changing permissions on ${savePath}`);
|
||||
await fs.promises.chmod(savePath, 0o755);
|
||||
|
||||
spinner.setMessage('Replacing old cli...');
|
||||
spinner.start();
|
||||
|
||||
// Delete old cli
|
||||
if (await pathExists(path.join(rootFolderHost, 'runtipi-cli'))) {
|
||||
this.logger.info('Deleting old cli...');
|
||||
await fs.promises.unlink(path.join(rootFolderHost, 'runtipi-cli'));
|
||||
}
|
||||
|
||||
// Delete VERSION file
|
||||
if (await pathExists(path.join(rootFolderHost, 'VERSION'))) {
|
||||
this.logger.info('Deleting VERSION file...');
|
||||
await fs.promises.unlink(path.join(rootFolderHost, 'VERSION'));
|
||||
}
|
||||
|
||||
// Rename downloaded cli to runtipi-cli
|
||||
this.logger.info('Renaming new cli to runtipi-cli...');
|
||||
await fs.promises.rename(savePath, path.join(rootFolderHost, 'runtipi-cli'));
|
||||
spinner.done('Old cli replaced');
|
||||
|
||||
// Wait for 3 second to make sure the old cli is gone
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
|
||||
this.logger.info('Starting new cli...');
|
||||
const childProcess = spawn('./runtipi-cli', [process.argv[1] as string, 'start']);
|
||||
|
||||
childProcess.stdout.on('data', (data) => {
|
||||
process.stdout.write(data);
|
||||
});
|
||||
|
||||
childProcess.stderr.on('data', (data) => {
|
||||
process.stderr.write(data);
|
||||
});
|
||||
|
||||
spinner.done(`Tipi ${targetVersion} successfully updated. Tipi is now starting, wait for this process to finish...`);
|
||||
|
||||
return { success: true, message: 'Tipi updated' };
|
||||
} catch (e) {
|
||||
spinner.fail('Tipi update failed, see logs for more details (logs/error.log)');
|
||||
return this.handleSystemError(e);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,182 +0,0 @@
|
|||
import crypto from 'crypto';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { envMapToString, envStringToMap, pathExists, settingsSchema } from '@runtipi/shared';
|
||||
import { logger } from '@/utils/logger/logger';
|
||||
|
||||
type EnvKeys =
|
||||
| 'APPS_REPO_ID'
|
||||
| 'APPS_REPO_URL'
|
||||
| 'TZ'
|
||||
| 'INTERNAL_IP'
|
||||
| 'DNS_IP'
|
||||
| 'ARCHITECTURE'
|
||||
| 'TIPI_VERSION'
|
||||
| 'JWT_SECRET'
|
||||
| 'ROOT_FOLDER_HOST'
|
||||
| 'NGINX_PORT'
|
||||
| 'NGINX_PORT_SSL'
|
||||
| 'DOMAIN'
|
||||
| 'STORAGE_PATH'
|
||||
| 'POSTGRES_PORT'
|
||||
| 'POSTGRES_HOST'
|
||||
| 'POSTGRES_DBNAME'
|
||||
| 'POSTGRES_PASSWORD'
|
||||
| 'POSTGRES_USERNAME'
|
||||
| 'REDIS_HOST'
|
||||
| 'REDIS_PASSWORD'
|
||||
| 'LOCAL_DOMAIN'
|
||||
| 'DEMO_MODE'
|
||||
| 'GUEST_DASHBOARD'
|
||||
| 'TIPI_GID'
|
||||
| 'TIPI_UID'
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
| (string & {});
|
||||
|
||||
/**
|
||||
* Reads and returns the generated seed
|
||||
*/
|
||||
const getSeed = async () => {
|
||||
const rootFolder = process.cwd();
|
||||
|
||||
const seedFilePath = path.join(rootFolder, 'state', 'seed');
|
||||
|
||||
if (!(await pathExists(seedFilePath))) {
|
||||
throw new Error('Seed file not found');
|
||||
}
|
||||
|
||||
const seed = await fs.promises.readFile(seedFilePath, 'utf-8');
|
||||
|
||||
return seed;
|
||||
};
|
||||
|
||||
/**
|
||||
* Derives a new entropy value from the provided entropy and the seed
|
||||
* @param {string} entropy - The entropy value to derive from
|
||||
*/
|
||||
const deriveEntropy = async (entropy: string) => {
|
||||
const seed = await getSeed();
|
||||
const hmac = crypto.createHmac('sha256', seed);
|
||||
hmac.update(entropy);
|
||||
|
||||
return hmac.digest('hex');
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a random seed if it does not exist yet
|
||||
*/
|
||||
const generateSeed = async (rootFolder: string) => {
|
||||
if (!(await pathExists(path.join(rootFolder, 'state', 'seed')))) {
|
||||
const randomBytes = crypto.randomBytes(32);
|
||||
const seed = randomBytes.toString('hex');
|
||||
|
||||
await fs.promises.writeFile(path.join(rootFolder, 'state', 'seed'), seed);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Will return the first internal IP address of the current system
|
||||
*/
|
||||
const getInternalIp = () => {
|
||||
const interfaces = os.networkInterfaces();
|
||||
|
||||
for (let i = 0; i < Object.keys(interfaces).length; i += 1) {
|
||||
const devName = Object.keys(interfaces)[i];
|
||||
const iface = interfaces[devName || ''];
|
||||
|
||||
const length = iface?.length || 0;
|
||||
for (let j = 0; j < length; j += 1) {
|
||||
const alias = iface?.[j];
|
||||
|
||||
if (alias && alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) return alias.address;
|
||||
}
|
||||
}
|
||||
|
||||
return '0.0.0.0';
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the architecture of the current system
|
||||
*/
|
||||
const getArchitecture = () => {
|
||||
const arch = os.arch();
|
||||
|
||||
if (arch === 'arm64') return 'arm64';
|
||||
if (arch === 'x64') return 'amd64';
|
||||
|
||||
throw new Error(`Unsupported architecture: ${arch}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a valid .env file from the settings.json file
|
||||
*/
|
||||
export const generateSystemEnvFile = async () => {
|
||||
const rootFolder = process.cwd();
|
||||
await fs.promises.mkdir(path.join(rootFolder, 'state'), { recursive: true });
|
||||
const settingsFilePath = path.join(rootFolder, 'state', 'settings.json');
|
||||
const envFilePath = path.join(rootFolder, '.env');
|
||||
|
||||
if (!(await pathExists(envFilePath))) {
|
||||
await fs.promises.writeFile(envFilePath, '');
|
||||
}
|
||||
|
||||
const envFile = await fs.promises.readFile(envFilePath, 'utf-8');
|
||||
const envMap: Map<EnvKeys, string> = envStringToMap(envFile);
|
||||
|
||||
if (!(await pathExists(settingsFilePath))) {
|
||||
await fs.promises.writeFile(settingsFilePath, JSON.stringify({}));
|
||||
}
|
||||
|
||||
const settingsFile = await fs.promises.readFile(settingsFilePath, 'utf-8');
|
||||
|
||||
const settings = settingsSchema.safeParse(JSON.parse(settingsFile));
|
||||
|
||||
if (!settings.success) {
|
||||
throw new Error(`Invalid settings.json file: ${settings.error.message}`);
|
||||
}
|
||||
|
||||
await generateSeed(rootFolder);
|
||||
|
||||
const { data } = settings;
|
||||
|
||||
const postgresPassword = envMap.get('POSTGRES_PASSWORD') || (await deriveEntropy('postgres_password'));
|
||||
const redisPassword = envMap.get('REDIS_PASSWORD') || (await deriveEntropy('redis_password'));
|
||||
|
||||
const version = await fs.promises.readFile(path.join(rootFolder, 'VERSION'), 'utf-8');
|
||||
|
||||
envMap.set('INTERNAL_IP', data.listenIp || getInternalIp());
|
||||
envMap.set('ARCHITECTURE', getArchitecture());
|
||||
envMap.set('TIPI_VERSION', version);
|
||||
envMap.set('ROOT_FOLDER_HOST', rootFolder);
|
||||
envMap.set('NGINX_PORT', String(data.port || 80));
|
||||
envMap.set('NGINX_PORT_SSL', String(data.sslPort || 443));
|
||||
envMap.set('STORAGE_PATH', data.storagePath || rootFolder);
|
||||
envMap.set('POSTGRES_PASSWORD', postgresPassword);
|
||||
envMap.set('POSTGRES_PORT', String(data.postgresPort || 5432));
|
||||
envMap.set('REDIS_HOST', 'tipi-redis');
|
||||
envMap.set('REDIS_PASSWORD', redisPassword);
|
||||
envMap.set('NODE_ENV', 'production');
|
||||
envMap.set('DOMAIN', data.domain || 'example.com');
|
||||
envMap.set('LOCAL_DOMAIN', data.localDomain || 'tipi.lan');
|
||||
|
||||
await fs.promises.writeFile(envFilePath, envMapToString(envMap));
|
||||
|
||||
return envMap;
|
||||
};
|
||||
|
||||
/**
|
||||
* Copies the system files from the assets folder to the current working directory
|
||||
*/
|
||||
export const copySystemFiles = async () => {
|
||||
// Remove old unused files
|
||||
const assetsFolder = path.join('/snapshot', 'runtipi', 'packages', 'cli', 'assets');
|
||||
|
||||
// Copy docker-compose.yml file
|
||||
logger.info('Copying file docker-compose.yml');
|
||||
await fs.promises.copyFile(path.join(assetsFolder, 'docker-compose.yml'), path.join(process.cwd(), 'docker-compose.yml'));
|
||||
|
||||
// Copy VERSION file
|
||||
logger.info('Copying file VERSION');
|
||||
await fs.promises.copyFile(path.join(assetsFolder, 'VERSION'), path.join(process.cwd(), 'VERSION'));
|
||||
};
|
|
@ -1,92 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
import { program } from 'commander';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { description, version } from '../package.json';
|
||||
import { AppExecutors, SystemExecutors } from './executors';
|
||||
|
||||
const main = async () => {
|
||||
program.description(description).version(version);
|
||||
|
||||
program.name('./runtipi-cli').usage('<command> [options]');
|
||||
|
||||
program
|
||||
.command('start')
|
||||
.description('Start tipi')
|
||||
.addHelpText('after', '\nExample call: sudo ./runtipi-cli start')
|
||||
.action(async () => {
|
||||
const systemExecutors = new SystemExecutors();
|
||||
await systemExecutors.start();
|
||||
});
|
||||
|
||||
program
|
||||
.command('stop')
|
||||
.description('Stop tipi')
|
||||
.action(async () => {
|
||||
const systemExecutors = new SystemExecutors();
|
||||
await systemExecutors.stop();
|
||||
});
|
||||
|
||||
program
|
||||
.command('restart')
|
||||
.description('Restart tipi')
|
||||
.action(async () => {
|
||||
const systemExecutors = new SystemExecutors();
|
||||
await systemExecutors.restart();
|
||||
});
|
||||
|
||||
program
|
||||
.command('update')
|
||||
.description('Update tipi')
|
||||
.argument('<target>', 'Target to update')
|
||||
.action(async (target) => {
|
||||
const systemExecutors = new SystemExecutors();
|
||||
await systemExecutors.update(target);
|
||||
});
|
||||
|
||||
program
|
||||
.command('reset-password')
|
||||
.description('Reset password')
|
||||
.action(async () => {
|
||||
const systemExecutors = new SystemExecutors();
|
||||
await systemExecutors.resetPassword();
|
||||
console.log(chalk.green('✓'), 'Password reset request created. Head back to the dashboard to set a new password.');
|
||||
});
|
||||
|
||||
program
|
||||
.command('clean-logs')
|
||||
.description('Clean logs')
|
||||
.action(async () => {
|
||||
const systemExecutors = new SystemExecutors();
|
||||
await systemExecutors.cleanLogs();
|
||||
});
|
||||
|
||||
// Start app: ./cli app start <app>
|
||||
// Stop app: ./cli app stop <app>
|
||||
program
|
||||
.command('app [command] <app>')
|
||||
.addHelpText('after', '\nExample call: sudo ./runtipi-cli app start <app>')
|
||||
.description('App management')
|
||||
.action(async (command, app) => {
|
||||
const appExecutors = new AppExecutors();
|
||||
switch (command) {
|
||||
case 'start':
|
||||
await appExecutors.startApp(app);
|
||||
break;
|
||||
case 'stop':
|
||||
await appExecutors.stopApp(app);
|
||||
break;
|
||||
default:
|
||||
console.log(chalk.red('✗'), 'Unknown command');
|
||||
}
|
||||
});
|
||||
|
||||
program.parse(process.argv);
|
||||
};
|
||||
|
||||
try {
|
||||
console.log(chalk.green('Welcome to Tipi CLI ✨'));
|
||||
main();
|
||||
} catch (e) {
|
||||
console.error('An error occurred:', e);
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
import { z } from 'zod';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
dotenv.config({ path: '.env.dev', override: true });
|
||||
} else {
|
||||
dotenv.config({ override: true });
|
||||
}
|
||||
|
||||
const environmentSchema = z
|
||||
.object({
|
||||
STORAGE_PATH: z.string(),
|
||||
ROOT_FOLDER_HOST: z.string(),
|
||||
APPS_REPO_ID: z.string(),
|
||||
ARCHITECTURE: z.enum(['arm64', 'amd64']),
|
||||
INTERNAL_IP: z.string().ip().or(z.literal('localhost')),
|
||||
TIPI_VERSION: z.string(),
|
||||
REDIS_PASSWORD: z.string(),
|
||||
POSTGRES_PORT: z.string(),
|
||||
POSTGRES_USERNAME: z.string(),
|
||||
POSTGRES_PASSWORD: z.string(),
|
||||
POSTGRES_DBNAME: z.string(),
|
||||
})
|
||||
.transform((env) => {
|
||||
const { STORAGE_PATH, ARCHITECTURE, ROOT_FOLDER_HOST, APPS_REPO_ID, INTERNAL_IP, TIPI_VERSION, REDIS_PASSWORD, POSTGRES_DBNAME, POSTGRES_PASSWORD, POSTGRES_USERNAME, POSTGRES_PORT, ...rest } =
|
||||
env;
|
||||
|
||||
return {
|
||||
storagePath: STORAGE_PATH,
|
||||
rootFolderHost: ROOT_FOLDER_HOST,
|
||||
appsRepoId: APPS_REPO_ID,
|
||||
arch: ARCHITECTURE,
|
||||
tipiVersion: TIPI_VERSION,
|
||||
internalIp: INTERNAL_IP,
|
||||
redisPassword: REDIS_PASSWORD,
|
||||
postgresPort: POSTGRES_PORT,
|
||||
postgresUsername: POSTGRES_USERNAME,
|
||||
postgresPassword: POSTGRES_PASSWORD,
|
||||
postgresDatabase: POSTGRES_DBNAME,
|
||||
...rest,
|
||||
};
|
||||
});
|
||||
|
||||
export const getEnv = () => environmentSchema.parse(process.env);
|
|
@ -1,19 +0,0 @@
|
|||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
type ExecAsyncParams = [command: string];
|
||||
|
||||
type ExecResult = { stdout: string; stderr: string };
|
||||
|
||||
export const execAsync = async (...args: ExecAsyncParams): Promise<ExecResult> => {
|
||||
try {
|
||||
const { stdout, stderr } = await promisify(exec)(...args);
|
||||
return { stdout, stderr };
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return { stderr: error.message, stdout: '' };
|
||||
}
|
||||
|
||||
return { stderr: String(error), stdout: '' };
|
||||
}
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
import { FileLogger } from '@runtipi/shared';
|
||||
import path from 'node:path';
|
||||
|
||||
export const logger = new FileLogger('cli', path.join(process.cwd(), 'logs'));
|
|
@ -1,61 +0,0 @@
|
|||
import logUpdate from 'log-update';
|
||||
import chalk from 'chalk';
|
||||
import { dots } from 'cli-spinners';
|
||||
|
||||
export class TerminalSpinner {
|
||||
message: string;
|
||||
|
||||
frame = 0;
|
||||
|
||||
interval: NodeJS.Timeout | null = null;
|
||||
|
||||
start() {
|
||||
this.interval = setInterval(() => {
|
||||
// eslint-disable-next-line no-plusplus
|
||||
this.frame = ++this.frame % dots.frames.length;
|
||||
logUpdate(`${dots.frames[this.frame]} ${this.message}`);
|
||||
}, dots.interval);
|
||||
}
|
||||
|
||||
constructor(message: string) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
setMessage(message: string) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
done(message?: string) {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
if (message) {
|
||||
logUpdate(chalk.green('✓'), message);
|
||||
} else {
|
||||
logUpdate.clear();
|
||||
}
|
||||
|
||||
logUpdate.done();
|
||||
}
|
||||
|
||||
fail(message?: string) {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
if (message) {
|
||||
logUpdate(chalk.red('✗'), message);
|
||||
} else {
|
||||
logUpdate.clear();
|
||||
}
|
||||
|
||||
logUpdate.done();
|
||||
}
|
||||
|
||||
log(message: string) {
|
||||
logUpdate(message);
|
||||
|
||||
logUpdate.done();
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
import { faker } from '@faker-js/faker';
|
||||
import fs from 'fs';
|
||||
import { APP_CATEGORIES, AppInfo, appInfoSchema } from '@runtipi/shared';
|
||||
import { getEnv } from '@/utils/environment/environment';
|
||||
|
||||
export const createAppConfig = (props?: Partial<AppInfo>, isInstalled = true) => {
|
||||
const { rootFolderHost, storagePath } = getEnv();
|
||||
|
||||
const appInfo = appInfoSchema.parse({
|
||||
id: faker.string.alphanumeric(32),
|
||||
available: true,
|
||||
port: faker.number.int({ min: 30, max: 65535 }),
|
||||
name: faker.string.alphanumeric(32),
|
||||
description: faker.string.alphanumeric(32),
|
||||
tipi_version: 1,
|
||||
short_desc: faker.string.alphanumeric(32),
|
||||
author: faker.string.alphanumeric(32),
|
||||
source: faker.internet.url(),
|
||||
categories: [APP_CATEGORIES.AUTOMATION],
|
||||
...props,
|
||||
});
|
||||
|
||||
const mockFiles: Record<string, string | string[]> = {};
|
||||
mockFiles[`${rootFolderHost}/.env`] = 'TEST=test';
|
||||
mockFiles[`${rootFolderHost}/repos/repo-id/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfoSchema.parse(appInfo));
|
||||
mockFiles[`${rootFolderHost}/repos/repo-id/apps/${appInfo.id}/docker-compose.yml`] = 'compose';
|
||||
mockFiles[`${rootFolderHost}/repos/repo-id/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
|
||||
|
||||
if (isInstalled) {
|
||||
mockFiles[`${rootFolderHost}/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfoSchema.parse(appInfo));
|
||||
mockFiles[`${rootFolderHost}/apps/${appInfo.id}/docker-compose.yml`] = 'compose';
|
||||
mockFiles[`${rootFolderHost}/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
|
||||
mockFiles[`${storagePath}/app-data/${appInfo.id}/data/test.txt`] = 'data';
|
||||
}
|
||||
|
||||
// @ts-expect-error - custom mock method
|
||||
fs.__applyMockFiles(mockFiles);
|
||||
|
||||
return appInfo;
|
||||
};
|
|
@ -1,41 +0,0 @@
|
|||
import { fs, vol } from 'memfs';
|
||||
|
||||
const copyFolderRecursiveSync = (src: string, dest: string) => {
|
||||
const exists = vol.existsSync(src);
|
||||
const stats = vol.statSync(src);
|
||||
const isDirectory = exists && stats.isDirectory();
|
||||
if (isDirectory) {
|
||||
vol.mkdirSync(dest, { recursive: true });
|
||||
vol.readdirSync(src).forEach((childItemName) => {
|
||||
copyFolderRecursiveSync(`${src}/${childItemName}`, `${dest}/${childItemName}`);
|
||||
});
|
||||
} else {
|
||||
vol.copyFileSync(src, dest);
|
||||
}
|
||||
};
|
||||
|
||||
export const fsMock = {
|
||||
default: {
|
||||
...fs,
|
||||
promises: {
|
||||
...fs.promises,
|
||||
cp: copyFolderRecursiveSync,
|
||||
},
|
||||
copySync: (src: string, dest: string) => {
|
||||
copyFolderRecursiveSync(src, dest);
|
||||
},
|
||||
__resetAllMocks: () => {
|
||||
vol.reset();
|
||||
},
|
||||
__applyMockFiles: (newMockFiles: Record<string, string>) => {
|
||||
// Create folder tree
|
||||
vol.fromJSON(newMockFiles, 'utf8');
|
||||
},
|
||||
__createMockFiles: (newMockFiles: Record<string, string>) => {
|
||||
vol.reset();
|
||||
// Create folder tree
|
||||
vol.fromJSON(newMockFiles, 'utf8');
|
||||
},
|
||||
__printVol: () => console.log(vol.toTree()),
|
||||
},
|
||||
};
|
|
@ -1,34 +0,0 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { vi, beforeEach } from 'vitest';
|
||||
import { getEnv } from '@/utils/environment/environment';
|
||||
|
||||
vi.mock('@runtipi/shared', async (importOriginal) => {
|
||||
const mod = (await importOriginal()) as object;
|
||||
|
||||
return {
|
||||
...mod,
|
||||
createLogger: vi.fn().mockReturnValue({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('fs', async () => {
|
||||
const { fsMock } = await import('@/tests/mocks/fs');
|
||||
return {
|
||||
...fsMock,
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// @ts-expect-error - custom mock method
|
||||
fs.__resetAllMocks();
|
||||
|
||||
const { rootFolderHost, appsRepoId } = getEnv();
|
||||
|
||||
await fs.promises.mkdir(path.join(rootFolderHost, 'state'), { recursive: true });
|
||||
await fs.promises.writeFile(path.join(rootFolderHost, 'state', 'seed'), 'seed');
|
||||
await fs.promises.mkdir(path.join(rootFolderHost, 'repos', appsRepoId, 'apps'), { recursive: true });
|
||||
});
|
|
@ -1,52 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"baseUrl": ".",
|
||||
"outDir": "./dist",
|
||||
"paths": {
|
||||
"@/utils/*": [
|
||||
"./src/utils/*"
|
||||
],
|
||||
"@/executors": [
|
||||
"./src/executors"
|
||||
],
|
||||
"@/tests/*": [
|
||||
"./tests/*"
|
||||
],
|
||||
},
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"strictNullChecks": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.mjs",
|
||||
"**/*.js",
|
||||
"**/*.jsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tsconfigPaths()],
|
||||
test: {
|
||||
setupFiles: ['./tests/vite.setup.ts'],
|
||||
coverage: { all: true, reporter: ['lcov', 'text-summary'] },
|
||||
},
|
||||
});
|
5
packages/dashboard/.dockerignore
Normal file
5
packages/dashboard/.dockerignore
Normal file
|
@ -0,0 +1,5 @@
|
|||
node_modules/
|
||||
.next/
|
||||
dist/
|
||||
sessions/
|
||||
logs/
|
|
@ -2,4 +2,3 @@
|
|||
.eslintrc.js
|
||||
next.config.js
|
||||
jest.config.js
|
||||
packages/
|
20
packages/dashboard/.eslintrc.js
Normal file
20
packages/dashboard/.eslintrc.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
module.exports = {
|
||||
extends: ['next/core-web-vitals', 'airbnb-typescript', 'eslint:recommended', 'plugin:import/typescript'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: './tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
plugins: ['@typescript-eslint', 'import'],
|
||||
rules: {
|
||||
'arrow-body-style': 0,
|
||||
'no-restricted-exports': 0,
|
||||
'max-len': [1, { code: 200 }],
|
||||
'import/extensions': ['error', 'ignorePackages', { js: 'never', jsx: 'never', ts: 'never', tsx: 'never' }],
|
||||
},
|
||||
globals: {
|
||||
JSX: true,
|
||||
},
|
||||
};
|
35
packages/dashboard/.gitignore
vendored
Normal file
35
packages/dashboard/.gitignore
vendored
Normal file
|
@ -0,0 +1,35 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
34
packages/dashboard/README.md
Normal file
34
packages/dashboard/README.md
Normal file
|
@ -0,0 +1,34 @@
|
|||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
|
||||
|
||||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
9
packages/dashboard/codegen.yml
Normal file
9
packages/dashboard/codegen.yml
Normal file
|
@ -0,0 +1,9 @@
|
|||
overwrite: true
|
||||
schema: "http://localhost:3001/graphql"
|
||||
documents: "src/graphql/**/*.graphql"
|
||||
generates:
|
||||
src/generated/graphql.tsx:
|
||||
plugins:
|
||||
- "typescript"
|
||||
- "typescript-operations"
|
||||
- "typescript-react-apollo"
|
11
packages/dashboard/jest.config.js
Normal file
11
packages/dashboard/jest.config.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
module.exports = {
|
||||
verbose: true,
|
||||
// testEnvironment: 'node',
|
||||
testMatch: ['**/__tests__/**/*.test.ts'],
|
||||
// setupFiles: ['<rootDir>/tests/dotenv-config.ts'],
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ['src/**/*.{ts,tsx}'],
|
||||
// coverageProvider: 'v8',
|
||||
passWithNoTests: true,
|
||||
};
|
15
packages/dashboard/next.config.js
Normal file
15
packages/dashboard/next.config.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
webpackDevMiddleware: (config) => {
|
||||
config.watchOptions = {
|
||||
poll: 1000,
|
||||
aggregateTimeout: 300,
|
||||
};
|
||||
return config;
|
||||
},
|
||||
reactStrictMode: true,
|
||||
basePath: '/dashboard',
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
65
packages/dashboard/package.json
Normal file
65
packages/dashboard/package.json
Normal file
|
@ -0,0 +1,65 @@
|
|||
{
|
||||
"name": "dashboard",
|
||||
"version": "0.7.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "jest --colors",
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"lint:fix": "next lint --fix",
|
||||
"gen": "graphql-codegen --config codegen.yml"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.6.8",
|
||||
"@chakra-ui/react": "^2.1.2",
|
||||
"@emotion/react": "^11",
|
||||
"@emotion/styled": "^11",
|
||||
"@fontsource/open-sans": "^4.5.8",
|
||||
"clsx": "^1.1.1",
|
||||
"final-form": "^4.20.6",
|
||||
"framer-motion": "^6",
|
||||
"graphql": "^15.8.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"next": "12.3.1",
|
||||
"react": "18.1.0",
|
||||
"react-dom": "18.1.0",
|
||||
"react-final-form": "^6.5.9",
|
||||
"react-icons": "^4.3.1",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-select": "^5.3.2",
|
||||
"remark-breaks": "^3.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-mdx": "^2.1.1",
|
||||
"swr": "^1.3.0",
|
||||
"tslib": "^2.4.0",
|
||||
"validator": "^13.7.0",
|
||||
"zustand": "^3.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0",
|
||||
"@graphql-codegen/cli": "^2.6.2",
|
||||
"@graphql-codegen/typescript": "^2.5.1",
|
||||
"@graphql-codegen/typescript-operations": "^2.4.2",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.2.16",
|
||||
"@types/js-cookie": "^3.0.2",
|
||||
"@types/node": "17.0.31",
|
||||
"@types/react": "18.0.8",
|
||||
"@types/react-dom": "18.0.3",
|
||||
"@types/react-slick": "^0.23.8",
|
||||
"@types/validator": "^13.7.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.18.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"autoprefixer": "^10.4.4",
|
||||
"eslint": "8.12.0",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-next": "12.1.4",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"jest": "^28.1.0",
|
||||
"postcss": "^8.4.12",
|
||||
"tailwindcss": "^3.0.23",
|
||||
"ts-jest": "^28.0.2",
|
||||
"typescript": "4.6.4"
|
||||
}
|
||||
}
|
6
packages/dashboard/postcss.config.js
Normal file
6
packages/dashboard/postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue