Compare commits
62 commits
Author | SHA1 | Date | |
---|---|---|---|
|
c962f030e3 | ||
|
1f5701120f | ||
|
249103f0a6 | ||
|
bb1f343ac9 | ||
|
6b371de137 | ||
|
7133a2a2dc | ||
|
44d40b4a34 | ||
|
03e2604ca0 | ||
|
ba3d860176 | ||
|
250e78450f | ||
|
d11299eeb8 | ||
|
6cd7ca1a4e | ||
|
bb9f26b2b1 | ||
|
30890bedd5 | ||
|
3876e8ee55 | ||
|
92d5a7b6a3 | ||
|
ca3dc64fd4 | ||
|
59dbe672c8 | ||
|
42349c5a27 | ||
|
8104a9f3f7 | ||
|
83e96cfd31 | ||
|
4d69fc4cff | ||
|
37c551da1f | ||
|
852128f551 | ||
|
bffa31c0b7 | ||
|
0f129a7809 | ||
|
79b448adf3 | ||
|
dffa3ed670 | ||
|
ef1ac3633b | ||
|
e520afdeff | ||
|
52499cb0bd | ||
|
cfeb9d4e19 | ||
|
a1515ac7b8 | ||
|
396d08dde0 | ||
|
3c7dcfeb5e | ||
|
9b5bcc7147 | ||
|
5c276bd235 | ||
|
c1723bb36b | ||
|
31c3de78b8 | ||
|
197f6e3998 | ||
|
abff9a4d5a | ||
|
91a361add1 | ||
|
68b3e4e8bd | ||
|
f893cf482d | ||
|
88878fccda | ||
|
64325150d5 | ||
|
203db0160a | ||
|
0261cf577c | ||
|
166d9b49f2 | ||
|
213b9ed482 | ||
|
de3141e1f8 | ||
|
6ec79ac1e8 | ||
|
5dbc996b92 | ||
|
f76af65212 | ||
|
1357efa0e3 | ||
|
4da6193ae9 | ||
|
f61e6b2dae | ||
|
230ae0a412 | ||
|
7c967271d7 | ||
|
3d85051915 | ||
|
467fc070d9 | ||
|
df59d21ce7 |
43 changed files with 661 additions and 490 deletions
|
@ -382,6 +382,24 @@
|
|||
"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,
|
||||
|
|
42
.github/workflows/alpha-release.yml
vendored
42
.github/workflows/alpha-release.yml
vendored
|
@ -11,20 +11,20 @@ jobs:
|
|||
create-tag:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tagname: ${{ steps.create_tag.outputs.tagname }}
|
||||
|
||||
tagname: ${{ steps.get_tag.outputs.tagname }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create Tag
|
||||
id: create_tag
|
||||
uses: butlerlogic/action-autotag@stable
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
- 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_prefix: 'v'
|
||||
tag_suffix: '-alpha.${{ github.event.inputs.tag }}'
|
||||
tag: ${{ steps.get_tag.outputs.tagname }}
|
||||
|
||||
build-worker:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -60,7 +60,6 @@ jobs:
|
|||
build-images:
|
||||
runs-on: ubuntu-latest
|
||||
needs: create-tag
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
@ -91,7 +90,6 @@ jobs:
|
|||
build-cli:
|
||||
runs-on: ubuntu-latest
|
||||
needs: create-tag
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
@ -99,7 +97,7 @@ jobs:
|
|||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v2.4.0
|
||||
name: Install pnpm
|
||||
|
@ -147,25 +145,21 @@ jobs:
|
|||
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: actions/create-release@v1
|
||||
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 }}
|
||||
release_name: ${{ needs.create-tag.outputs.tagname }}
|
||||
name: ${{ needs.create-tag.outputs.tagname }}
|
||||
draft: false
|
||||
prerelease: true
|
||||
|
||||
- name: Upload X64 Linux CLI binary to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: cli/bin/cli-x64
|
||||
asset_name: runtipi-cli-linux-x64
|
||||
asset_content_type: application/octet-stream
|
||||
files: |
|
||||
runtipi-cli-linux-x64
|
||||
|
|
111
.github/workflows/beta-release.yml
vendored
111
.github/workflows/beta-release.yml
vendored
|
@ -8,27 +8,57 @@ on:
|
|||
required: true
|
||||
|
||||
jobs:
|
||||
get-tag:
|
||||
create-tag:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tag: ${{ steps.get_tag.outputs.tag }}
|
||||
tagname: ${{ steps.get_tag.outputs.tagname }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Get tag from VERSION file
|
||||
- name: Get tag from package.json
|
||||
id: get_tag
|
||||
run: |
|
||||
VERSION=$(npm run version --silent)
|
||||
echo "tag=v${VERSION}-beta.${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
|
||||
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: get-tag
|
||||
needs: create-tag
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
@ -53,13 +83,13 @@ jobs:
|
|||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ghcr.io/${{ github.repository_owner }}/runtipi:${{ needs.get-tag.outputs.tag }}
|
||||
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: get-tag
|
||||
needs: create-tag
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
@ -67,7 +97,7 @@ jobs:
|
|||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v2.4.0
|
||||
name: Install pnpm
|
||||
|
@ -93,7 +123,7 @@ jobs:
|
|||
run: pnpm install
|
||||
|
||||
- name: Set version
|
||||
run: pnpm -r --filter cli set-version ${{ needs.get-tag.outputs.tag }}
|
||||
run: pnpm -r --filter cli set-version ${{ needs.create-tag.outputs.tagname }}
|
||||
|
||||
- name: Build CLI
|
||||
run: pnpm -r --filter cli package
|
||||
|
@ -104,28 +134,9 @@ jobs:
|
|||
name: cli
|
||||
path: packages/cli/dist
|
||||
|
||||
create-tag:
|
||||
needs: [build-images, build-cli]
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tagname: ${{ steps.create_tag.outputs.tagname }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create Tag
|
||||
id: create_tag
|
||||
uses: butlerlogic/action-autotag@stable
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
with:
|
||||
tag_prefix: 'v'
|
||||
tag_suffix: '-beta.${{ github.event.inputs.tag }}'
|
||||
|
||||
publish-release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [create-tag, build-images, build-cli]
|
||||
needs: [create-tag, build-images, build-cli, build-worker]
|
||||
outputs:
|
||||
id: ${{ steps.create_release.outputs.id }}
|
||||
steps:
|
||||
|
@ -135,38 +146,26 @@ jobs:
|
|||
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: actions/create-release@v1
|
||||
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 }}
|
||||
release_name: ${{ needs.create-tag.outputs.tagname }}
|
||||
name: ${{ needs.create-tag.outputs.tagname }}
|
||||
draft: false
|
||||
prerelease: true
|
||||
|
||||
- name: Upload X64 Linux CLI binary to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: cli/bin/cli-x64
|
||||
asset_name: runtipi-cli-linux-x64
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload ARM64 Linux CLI binary to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: cli/bin/cli-arm64
|
||||
asset_name: runtipi-cli-linux-arm64
|
||||
asset_content_type: application/octet-stream
|
||||
files: |
|
||||
runtipi-cli-linux-x64
|
||||
runtipi-cli-linux-arm64
|
||||
|
||||
e2e-tests:
|
||||
needs: [create-tag, publish-release]
|
||||
|
|
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
@ -43,7 +43,7 @@ jobs:
|
|||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v2.4.0
|
||||
name: Install pnpm
|
||||
|
@ -106,7 +106,7 @@ jobs:
|
|||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v2.4.0
|
||||
name: Install pnpm
|
||||
|
|
8
.github/workflows/e2e.yml
vendored
8
.github/workflows/e2e.yml
vendored
|
@ -72,9 +72,6 @@ jobs:
|
|||
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: Wait 1 minute for Droplet to be ready
|
||||
run: sleep 60
|
||||
|
||||
- name: Create docker group on Droplet
|
||||
uses: fifsky/ssh-action@master
|
||||
with:
|
||||
|
@ -85,6 +82,9 @@ jobs:
|
|||
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:
|
||||
|
@ -134,7 +134,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- name: Create .env.e2e file with Droplet IP
|
||||
run: |
|
||||
|
|
110
.github/workflows/release.yml
vendored
110
.github/workflows/release.yml
vendored
|
@ -3,28 +3,28 @@ on:
|
|||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
get-tag:
|
||||
create-tag:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-images, build-cli]
|
||||
outputs:
|
||||
tag: ${{ steps.get_tag.outputs.tag }}
|
||||
tagname: ${{ steps.get_tag.outputs.tagname }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Get tag from VERSION file
|
||||
- name: Get tag from package.json
|
||||
id: get_tag
|
||||
run: |
|
||||
VERSION=$(npm run version --silent)
|
||||
echo "tag=v${VERSION}" >> $GITHUB_OUTPUT
|
||||
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: get-tag
|
||||
needs: create-tag
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
@ -49,14 +49,45 @@ jobs:
|
|||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ghcr.io/${{ github.repository_owner }}/runtipi:${{ needs.get-tag.outputs.tag }},ghcr.io/${{ github.repository_owner }}/runtipi:latest
|
||||
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
|
||||
|
||||
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 }},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: get-tag
|
||||
needs: create-tag
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
@ -64,7 +95,7 @@ jobs:
|
|||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v2.4.0
|
||||
name: Install pnpm
|
||||
|
@ -90,7 +121,7 @@ jobs:
|
|||
run: pnpm install
|
||||
|
||||
- name: Set version
|
||||
run: pnpm -r --filter cli set-version ${{ needs.get-tag.outputs.tag }}
|
||||
run: pnpm -r --filter cli set-version ${{ needs.create-tag.outputs.tagname }}
|
||||
|
||||
- name: Build CLI
|
||||
run: pnpm -r --filter cli package
|
||||
|
@ -101,26 +132,9 @@ jobs:
|
|||
name: cli
|
||||
path: packages/cli/dist
|
||||
|
||||
create-tag:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-images, build-cli]
|
||||
outputs:
|
||||
tagname: ${{ steps.create_tag.outputs.tagname }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create Tag
|
||||
id: create_tag
|
||||
uses: butlerlogic/action-autotag@stable
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
with:
|
||||
tag_prefix: 'v'
|
||||
|
||||
publish-release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [create-tag]
|
||||
needs: [create-tag, build-images, build-worker, build-cli]
|
||||
outputs:
|
||||
id: ${{ steps.create_release.outputs.id }}
|
||||
steps:
|
||||
|
@ -130,38 +144,26 @@ jobs:
|
|||
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
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
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 }}
|
||||
release_name: ${{ needs.create-tag.outputs.tagname }}
|
||||
name: ${{ needs.create-tag.outputs.tagname }}
|
||||
draft: false
|
||||
prerelease: true
|
||||
|
||||
- name: Upload X64 Linux CLI binary to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: cli/bin/cli-x64
|
||||
asset_name: runtipi-cli-linux-x64
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload ARM64 Linux CLI binary to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: cli/bin/cli-arm64
|
||||
asset_name: runtipi-cli-linux-arm64
|
||||
asset_content_type: application/octet-stream
|
||||
files: |
|
||||
runtipi-cli-linux-x64
|
||||
runtipi-cli-linux-arm64
|
||||
|
||||
e2e-tests:
|
||||
needs: [create-tag, publish-release]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
ARG NODE_VERSION="18.16"
|
||||
ARG NODE_VERSION="20.10"
|
||||
ARG ALPINE_VERSION="3.18"
|
||||
|
||||
FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS node_base
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
ARG NODE_VERSION="18.16"
|
||||
ARG NODE_VERSION="20.10"
|
||||
ARG ALPINE_VERSION="3.18"
|
||||
|
||||
FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Tipi — A personal homeserver for everyone
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[![All Contributors](https://img.shields.io/badge/all_contributors-40-orange.svg?style=flat-square)](#contributors-)
|
||||
[![All Contributors](https://img.shields.io/badge/all_contributors-42-orange.svg?style=flat-square)](#contributors-)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
[![License](https://img.shields.io/github/license/runtipi/runtipi)](https://github.com/runtipi/runtipi/blob/master/LICENSE)
|
||||
|
@ -123,6 +123,8 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||
<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>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -60,7 +60,6 @@ services:
|
|||
context: .
|
||||
dockerfile: ./packages/worker/Dockerfile.dev
|
||||
container_name: tipi-worker
|
||||
user: root
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '-f', 'http://localhost:3000/healthcheck']
|
||||
interval: 5s
|
||||
|
|
16
package.json
16
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "runtipi",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1",
|
||||
"description": "A homeserver for everyone",
|
||||
"scripts": {
|
||||
"knip": "knip",
|
||||
|
@ -45,13 +45,15 @@
|
|||
"@runtipi/postgres-migrations": "^5.3.0",
|
||||
"@runtipi/shared": "workspace:^",
|
||||
"@tabler/core": "1.0.0-beta20",
|
||||
"@tabler/icons-react": "^2.40.0",
|
||||
"@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",
|
||||
|
@ -65,7 +67,7 @@
|
|||
"react-hot-toast": "^2.4.1",
|
||||
"react-markdown": "^9.0.0",
|
||||
"react-select": "^5.8.0",
|
||||
"react-tooltip": "^5.22.0",
|
||||
"react-tooltip": "^5.25.0",
|
||||
"redaxios": "^0.5.1",
|
||||
"redis": "^4.6.10",
|
||||
"rehype-raw": "^7.0.0",
|
||||
|
@ -93,16 +95,16 @@
|
|||
"@total-typescript/shoehorn": "^0.1.1",
|
||||
"@total-typescript/ts-reset": "^0.5.1",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/jest": "^29.5.7",
|
||||
"@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.34",
|
||||
"@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.9.1",
|
||||
"@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",
|
||||
|
@ -110,7 +112,7 @@
|
|||
"eslint": "8.52.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.1.0",
|
||||
"eslint-config-next": "14.0.1",
|
||||
"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",
|
||||
|
|
|
@ -21,7 +21,7 @@ services:
|
|||
tipi-db:
|
||||
container_name: tipi-db
|
||||
image: postgres:14
|
||||
restart: on-failure
|
||||
restart: unless-stopped
|
||||
stop_grace_period: 1m
|
||||
ports:
|
||||
- ${POSTGRES_PORT:-5432}:5432
|
||||
|
@ -60,7 +60,6 @@ services:
|
|||
container_name: tipi-worker
|
||||
image: ghcr.io/runtipi/worker:${TIPI_VERSION}
|
||||
restart: unless-stopped
|
||||
user: root
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '-f', 'http://localhost:3000/healthcheck']
|
||||
interval: 5s
|
||||
|
|
|
@ -8,7 +8,7 @@ async function bundle() {
|
|||
entryPoints: ['./src/index.ts'],
|
||||
outfile: './dist/index.js',
|
||||
platform: 'node',
|
||||
target: 'node18',
|
||||
target: 'node20',
|
||||
bundle: true,
|
||||
color: true,
|
||||
sourcemap: commandArgs.includes('--sourcemap'),
|
||||
|
|
|
@ -8,10 +8,10 @@
|
|||
"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 node18-darwin-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=node18 --outfile=dist/index.js --metafile=meta.json --analyze",
|
||||
"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",
|
||||
|
@ -20,8 +20,8 @@
|
|||
"pkg": {
|
||||
"assets": "assets/**/*",
|
||||
"targets": [
|
||||
"node18-linux-x64",
|
||||
"node18-linux-arm64"
|
||||
"node20-linux-x64",
|
||||
"node20-linux-arm64"
|
||||
],
|
||||
"outputPath": "dist/bin"
|
||||
},
|
||||
|
@ -30,7 +30,7 @@
|
|||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.2.0",
|
||||
"@types/cli-progress": "^3.11.4",
|
||||
"@types/cli-progress": "^3.11.5",
|
||||
"@types/node": "20.8.10",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"esbuild": "^0.19.4",
|
||||
|
|
|
@ -58,9 +58,17 @@ export const envSchema = z.object({
|
|||
if (typeof value === 'boolean') return value;
|
||||
return value === 'true';
|
||||
}),
|
||||
allowAutoThemes: z
|
||||
.string()
|
||||
.or(z.boolean())
|
||||
.optional()
|
||||
.transform((value) => {
|
||||
if (typeof value === 'boolean') return value;
|
||||
return value === 'true';
|
||||
}),
|
||||
});
|
||||
|
||||
export const settingsSchema = envSchema
|
||||
.partial()
|
||||
.pick({ dnsIp: true, internalIp: true, postgresPort: true, appsRepoUrl: true, domain: true, storagePath: true, localDomain: true, demoMode: true, guestDashboard: true })
|
||||
.pick({ dnsIp: true, internalIp: true, postgresPort: true, appsRepoUrl: true, domain: true, storagePath: true, localDomain: true, demoMode: true, guestDashboard: true, allowAutoThemes: true })
|
||||
.and(z.object({ port: z.number(), sslPort: z.number(), listenIp: z.string().ip().trim() }).partial());
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
ARG NODE_VERSION="18.16"
|
||||
ARG NODE_VERSION="20.10"
|
||||
ARG ALPINE_VERSION="3.18"
|
||||
|
||||
FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS node_base
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
ARG NODE_VERSION="18.16"
|
||||
ARG NODE_VERSION="20.10"
|
||||
ARG ALPINE_VERSION="3.18"
|
||||
|
||||
FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS node_base
|
||||
|
|
|
@ -8,7 +8,7 @@ async function bundle() {
|
|||
entryPoints: ['./src/index.ts'],
|
||||
outfile: './dist/index.js',
|
||||
platform: 'node',
|
||||
target: 'node18',
|
||||
target: 'node20',
|
||||
bundle: true,
|
||||
color: true,
|
||||
sourcemap: commandArgs.includes('--sourcemap'),
|
||||
|
|
|
@ -4,7 +4,7 @@ import path from 'node:path';
|
|||
import Redis from 'ioredis';
|
||||
import dotenv from 'dotenv';
|
||||
import { Queue } from 'bullmq';
|
||||
import { copySystemFiles, generateSystemEnvFile, generateTlsCertificates } from '@/lib/system';
|
||||
import { copySystemFiles, ensureFilePermissions, generateSystemEnvFile, generateTlsCertificates } from '@/lib/system';
|
||||
import { runPostgresMigrations } from '@/lib/migrations';
|
||||
import { startWorker } from './watcher/watcher';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
@ -30,6 +30,9 @@ const main = async () => {
|
|||
logger.info('Generating TLS certificates...');
|
||||
await generateTlsCertificates({ domain: envMap.get('LOCAL_DOMAIN') });
|
||||
|
||||
logger.info('Ensuring file permissions...');
|
||||
await ensureFilePermissions();
|
||||
|
||||
logger.info('Starting queue...');
|
||||
const queue = new Queue('events', { connection: { host: envMap.get('REDIS_HOST'), port: 6379, password: envMap.get('REDIS_PASSWORD') } });
|
||||
logger.info('Obliterating queue...');
|
||||
|
|
|
@ -259,16 +259,7 @@ export const generateTlsCertificates = async (data: { domain?: string }) => {
|
|||
};
|
||||
|
||||
export const ensureFilePermissions = async () => {
|
||||
const filesAndFolders = [
|
||||
path.join(ROOT_FOLDER, 'apps'),
|
||||
path.join(ROOT_FOLDER, 'logs'),
|
||||
path.join(ROOT_FOLDER, 'repos'),
|
||||
path.join(ROOT_FOLDER, 'state'),
|
||||
path.join(ROOT_FOLDER, 'traefik'),
|
||||
path.join(ROOT_FOLDER, '.env'),
|
||||
path.join(ROOT_FOLDER, 'VERSION'),
|
||||
path.join(ROOT_FOLDER, 'docker-compose.yml'),
|
||||
];
|
||||
const filesAndFolders = [path.join(ROOT_FOLDER, 'state'), path.join(ROOT_FOLDER, 'traefik')];
|
||||
|
||||
const files600 = [path.join(ROOT_FOLDER, 'traefik', 'shared', 'acme.json')];
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import path from 'path';
|
||||
import { execAsync, pathExists } from '@runtipi/shared';
|
||||
import { getRepoHash } from './repo.helpers';
|
||||
import { getRepoHash, getRepoBaseUrlAndBranch } from './repo.helpers';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
export class RepoExecutors {
|
||||
|
@ -26,21 +26,32 @@ export class RepoExecutors {
|
|||
/**
|
||||
* Given a repo url, clone it to the repos folder if it doesn't exist
|
||||
*
|
||||
* @param {string} repoUrl
|
||||
* @param {string} url
|
||||
*/
|
||||
public cloneRepo = async (repoUrl: string) => {
|
||||
public cloneRepo = async (url: string) => {
|
||||
try {
|
||||
const repoHash = getRepoHash(repoUrl);
|
||||
// We may have a potential branch computed in the hash (see getRepoBaseUrlAndBranch)
|
||||
// so we do it here before splitting the url into repoUrl and branch
|
||||
const repoHash = getRepoHash(url);
|
||||
const repoPath = path.join('/app', 'repos', repoHash);
|
||||
|
||||
if (await pathExists(repoPath)) {
|
||||
this.logger.info(`Repo ${repoUrl} already exists`);
|
||||
this.logger.info(`Repo ${url} already exists`);
|
||||
return { success: true, message: '' };
|
||||
}
|
||||
|
||||
this.logger.info(`Cloning repo ${repoUrl} to ${repoPath}`);
|
||||
const [repoUrl, branch] = getRepoBaseUrlAndBranch(url);
|
||||
|
||||
await execAsync(`git clone ${repoUrl} ${repoPath}`);
|
||||
let cloneCommand;
|
||||
if (branch) {
|
||||
this.logger.info(`Cloning repo ${repoUrl} on branch ${branch} to ${repoPath}`);
|
||||
cloneCommand = `git clone -b ${branch} ${repoUrl} ${repoPath}`;
|
||||
} else {
|
||||
this.logger.info(`Cloning repo ${repoUrl} to ${repoPath}`);
|
||||
cloneCommand = `git clone ${repoUrl} ${repoPath}`;
|
||||
}
|
||||
|
||||
await execAsync(cloneCommand);
|
||||
|
||||
this.logger.info(`Cloned repo ${repoUrl} to ${repoPath}`);
|
||||
return { success: true, message: '' };
|
||||
|
|
|
@ -10,3 +10,18 @@ export const getRepoHash = (repoUrl: string) => {
|
|||
hash.update(repoUrl);
|
||||
return hash.digest('hex');
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Extracts the base URL and branch from a repository URL.
|
||||
* @param repoUrl The repository URL.
|
||||
* @returns An array containing the base URL and branch, or just the base URL if no branch is found.
|
||||
*/
|
||||
export const getRepoBaseUrlAndBranch = (repoUrl: string) => {
|
||||
const branchMatch = repoUrl.match(/^(.*)\/tree\/(.*)$/);
|
||||
if (branchMatch) {
|
||||
return [branchMatch[1], branchMatch[2]] ;
|
||||
}
|
||||
|
||||
return [repoUrl, undefined] ;
|
||||
};
|
||||
|
|
570
pnpm-lock.yaml
570
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
BIN
public/tipi-christmas.png
Normal file
BIN
public/tipi-christmas.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
|
@ -121,16 +121,12 @@ if ! command -v docker >/dev/null; then
|
|||
install_docker "${OS}"
|
||||
docker_result=$?
|
||||
|
||||
if [[ docker_result -eq 0 ]]; then
|
||||
echo "Docker installed"
|
||||
else
|
||||
if [[ docker_result -ne 0 ]]; then
|
||||
echo "Your system ${OS} is not supported trying with sub_os ${SUB_OS}"
|
||||
install_docker "${SUB_OS}"
|
||||
docker_sub_result=$?
|
||||
|
||||
if [[ docker_sub_result -eq 0 ]]; then
|
||||
echo "Docker installed"
|
||||
else
|
||||
if [[ docker_sub_result -ne 0 ]]; then
|
||||
echo "Your system ${SUB_OS} is not supported please install docker manually"
|
||||
exit 1
|
||||
fi
|
||||
|
@ -138,11 +134,15 @@ if ! command -v docker >/dev/null; then
|
|||
|
||||
# Make sure user is in docker group
|
||||
if ! groups | grep -q '\bdocker\b'; then
|
||||
echo "Adding user to docker group"
|
||||
sudo usermod -aG docker "$USER"
|
||||
echo "✓ Docker installed. Please re-run the installation script to continue with the installation. (curl -L https://setup.runtipi.io | bash)"
|
||||
fi
|
||||
|
||||
# Reload user groups
|
||||
newgrp docker
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
||||
function check_dependency_and_install() {
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
import { getCurrentLocale } from 'src/utils/getCurrentLocale';
|
||||
import { getLogo } from '@/lib/themes';
|
||||
import { getConfig } from '@/server/core/TipiConfig';
|
||||
import { LanguageSelector } from '../components/LanguageSelector';
|
||||
|
||||
export default async function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
const locale = getCurrentLocale();
|
||||
const { allowAutoThemes } = getConfig();
|
||||
|
||||
return (
|
||||
<div className="page page-center">
|
||||
|
@ -15,7 +18,7 @@ export default async function AuthLayout({ children }: { children: React.ReactNo
|
|||
<div className="text-center mb-4">
|
||||
<Image
|
||||
alt="Tipi logo"
|
||||
src="/tipi.png"
|
||||
src={getLogo(allowAutoThemes)}
|
||||
height={50}
|
||||
width={50}
|
||||
style={{
|
||||
|
|
|
@ -94,11 +94,11 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app, localDomain }) => {
|
|||
|
||||
const updateMutation = useAction(updateAppAction, {
|
||||
onSuccess: (data) => {
|
||||
setCustomStatus(app.status);
|
||||
|
||||
if (!data.success) {
|
||||
setCustomStatus(app.status);
|
||||
toast.error(data.failure.reason);
|
||||
} else {
|
||||
setCustomStatus('stopped');
|
||||
toast.success(t('apps.app-details.update-success'));
|
||||
}
|
||||
},
|
||||
|
|
|
@ -26,7 +26,7 @@ export default async function Page() {
|
|||
|
||||
if (app.info?.available)
|
||||
return (
|
||||
<Link href={`/apps/${app.id}`} className={clsx('col-sm-6 col-lg-4', styles.link)} passHref>
|
||||
<Link key={app.id} href={`/apps/${app.id}`} className={clsx('col-sm-6 col-lg-4', styles.link)} passHref>
|
||||
<AppTile key={app.id} app={app.info} status={app.status} updateAvailable={updateAvailable} />
|
||||
</Link>
|
||||
);
|
||||
|
|
|
@ -13,14 +13,16 @@ import { useAction } from 'next-safe-action/hook';
|
|||
import { logoutAction } from '@/actions/logout/logout-action';
|
||||
import Script from 'next/script';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { getLogo } from '@/lib/themes';
|
||||
import { NavBar } from '../NavBar';
|
||||
|
||||
interface IProps {
|
||||
isUpdateAvailable?: boolean;
|
||||
authenticated?: boolean;
|
||||
autoTheme: boolean;
|
||||
}
|
||||
|
||||
export const Header: React.FC<IProps> = ({ isUpdateAvailable, authenticated = true }) => {
|
||||
export const Header: React.FC<IProps> = ({ isUpdateAvailable, authenticated = true, autoTheme }) => {
|
||||
const { setDarkMode } = useUIStore();
|
||||
const t = useTranslations('header');
|
||||
|
||||
|
@ -55,7 +57,7 @@ export const Header: React.FC<IProps> = ({ isUpdateAvailable, authenticated = tr
|
|||
className={clsx('navbar-brand-image me-3')}
|
||||
width={100}
|
||||
height={100}
|
||||
src="/tipi.png"
|
||||
src={getLogo(autoTheme)}
|
||||
style={{
|
||||
width: '30px',
|
||||
maxWidth: '30px',
|
||||
|
|
|
@ -5,6 +5,7 @@ import { SystemServiceClass } from '@/server/services/system';
|
|||
import semver from 'semver';
|
||||
import clsx from 'clsx';
|
||||
import { AppServiceClass } from '@/server/services/apps/apps.service';
|
||||
import { getConfig } from '@/server/core/TipiConfig';
|
||||
import { Header } from './components/Header';
|
||||
import { PageTitle } from './components/PageTitle';
|
||||
import styles from './layout.module.scss';
|
||||
|
@ -12,6 +13,7 @@ import { LayoutActions } from './components/LayoutActions/LayoutActions';
|
|||
|
||||
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||
const user = await getUserFromCookie();
|
||||
const { allowAutoThemes } = getConfig();
|
||||
|
||||
const { apps } = await AppServiceClass.listApps();
|
||||
|
||||
|
@ -25,7 +27,7 @@ export default async function DashboardLayout({ children }: { children: React.Re
|
|||
|
||||
return (
|
||||
<div className="page">
|
||||
<Header isUpdateAvailable={!isLatest} />
|
||||
<Header isUpdateAvailable={!isLatest} autoTheme={allowAutoThemes} />
|
||||
<div className="page-wrapper">
|
||||
<div className="page-header d-print-none">
|
||||
<div className="container-xl">
|
||||
|
|
|
@ -6,6 +6,7 @@ import { useTranslations } from 'next-intl';
|
|||
import { useAction } from 'next-safe-action/hook';
|
||||
import { updateSettingsAction } from '@/actions/settings/update-settings';
|
||||
import { Locale } from '@/shared/internationalization/locales';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { SettingsForm, SettingsFormValues } from '../SettingsForm';
|
||||
|
||||
type Props = {
|
||||
|
@ -16,12 +17,15 @@ type Props = {
|
|||
export const SettingsContainer = ({ initialValues, currentLocale }: Props) => {
|
||||
const t = useTranslations();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const updateSettingsMutation = useAction(updateSettingsAction, {
|
||||
onSuccess: (data) => {
|
||||
if (!data.success) {
|
||||
toast.error(data.failure.reason);
|
||||
} else {
|
||||
toast.success(t('settings.settings.settings-updated'));
|
||||
router.refresh();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -19,6 +19,7 @@ export type SettingsFormValues = {
|
|||
storagePath?: string;
|
||||
localDomain?: string;
|
||||
guestDashboard?: boolean;
|
||||
allowAutoThemes?: boolean;
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
|
@ -139,6 +140,29 @@ export const SettingsForm = (props: IProps) => {
|
|||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<Controller
|
||||
control={control}
|
||||
name="allowAutoThemes"
|
||||
defaultValue={false}
|
||||
render={({ field: { onChange, value, ref, ...rest } }) => (
|
||||
<Switch
|
||||
className="mb-3"
|
||||
ref={ref}
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
{...rest}
|
||||
label={
|
||||
<>
|
||||
{t('allow-auto-themes')}
|
||||
<Tooltip anchorSelect=".allow-auto-themes-hint">{t('allow-auto-themes-hint')}</Tooltip>
|
||||
<span className={clsx('ms-1 form-help allow-auto-themes-hint')}>?</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<Input
|
||||
{...register('domain')}
|
||||
|
|
|
@ -8,12 +8,15 @@ type Props = {
|
|||
children: React.ReactNode;
|
||||
cookies: ComponentProps<typeof CookiesProvider>['value'];
|
||||
initialTheme?: string;
|
||||
allowAutoThemes: boolean;
|
||||
};
|
||||
|
||||
export const ClientProviders = ({ children, initialTheme, cookies }: Props) => {
|
||||
export const ClientProviders = ({ children, initialTheme, cookies, allowAutoThemes }: Props) => {
|
||||
return (
|
||||
<CookiesProvider value={cookies}>
|
||||
<ThemeProvider initialTheme={initialTheme}>{children}</ThemeProvider>
|
||||
<ThemeProvider allowAutoThemes={allowAutoThemes} initialTheme={initialTheme}>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</CookiesProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,14 +3,22 @@
|
|||
import { useUIStore } from '@/client/state/uiStore';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useCookies } from 'next-client-cookies';
|
||||
import { getAutoTheme } from '@/lib/themes';
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
allowAutoThemes: boolean;
|
||||
initialTheme?: string;
|
||||
};
|
||||
|
||||
const loadChristmasTheme = async () => {
|
||||
const { default: LetItGo } = await import('let-it-go');
|
||||
const snow = new LetItGo({ number: 50 });
|
||||
snow.letItGoAgain();
|
||||
};
|
||||
|
||||
export const ThemeProvider = (props: Props) => {
|
||||
const { children, initialTheme } = props;
|
||||
const { children, initialTheme, allowAutoThemes } = props;
|
||||
const cookies = useCookies();
|
||||
const { theme, setDarkMode } = useUIStore();
|
||||
|
||||
|
@ -30,5 +38,12 @@ export const ThemeProvider = (props: Props) => {
|
|||
setDarkMode(cookieTheme === 'dark');
|
||||
}, [cookies, initialTheme, setDarkMode, theme]);
|
||||
|
||||
useEffect(() => {
|
||||
const autoTheme = getAutoTheme();
|
||||
if (autoTheme === 'christmas' && allowAutoThemes && typeof window !== 'undefined') {
|
||||
loadChristmasTheme();
|
||||
}
|
||||
}, [allowAutoThemes]);
|
||||
|
||||
return children;
|
||||
};
|
||||
|
|
|
@ -2,9 +2,10 @@ import React from 'react';
|
|||
import type { Metadata } from 'next';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
import { Inter } from 'next/font/google';
|
||||
import { GeistSans } from 'geist/font/sans';
|
||||
import merge from 'lodash.merge';
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import { getConfig } from '@/server/core/TipiConfig';
|
||||
|
||||
import './global.css';
|
||||
import clsx from 'clsx';
|
||||
|
@ -12,11 +13,6 @@ import { Toaster } from 'react-hot-toast';
|
|||
import { getCurrentLocale } from '../utils/getCurrentLocale';
|
||||
import { ClientProviders } from './components/ClientProviders';
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Tipi',
|
||||
description: 'Tipi',
|
||||
|
@ -31,10 +27,12 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
|||
|
||||
const theme = cookies().get('theme');
|
||||
|
||||
const { allowAutoThemes } = getConfig();
|
||||
|
||||
return (
|
||||
<html lang={locale} className={clsx(inter.className, 'border-top-wide border-primary')}>
|
||||
<html lang={locale} className={clsx(GeistSans.className, 'border-top-wide border-primary')}>
|
||||
<NextIntlClientProvider locale={locale} messages={mergedMessages}>
|
||||
<ClientProviders initialTheme={theme?.value} cookies={cookies().getAll()}>
|
||||
<ClientProviders initialTheme={theme?.value} cookies={cookies().getAll()} allowAutoThemes={allowAutoThemes}>
|
||||
<body data-bs-theme={theme?.value}>
|
||||
{children}
|
||||
<Toaster />
|
||||
|
|
|
@ -14,7 +14,7 @@ export const dynamic = 'force-dynamic';
|
|||
|
||||
export default async function RootPage() {
|
||||
const appService = new AppServiceClass(db);
|
||||
const { guestDashboard } = getConfig();
|
||||
const { guestDashboard, allowAutoThemes } = getConfig();
|
||||
|
||||
const headersList = headers();
|
||||
const host = headersList.get('host');
|
||||
|
@ -24,7 +24,7 @@ export default async function RootPage() {
|
|||
const apps = await appService.getGuestDashboardApps();
|
||||
|
||||
return (
|
||||
<UnauthenticatedPage title="guest-dashboard" subtitle="runtipi">
|
||||
<UnauthenticatedPage autoTheme={allowAutoThemes} title="guest-dashboard" subtitle="runtipi">
|
||||
{apps.length === 0 ? (
|
||||
<EmptyPage title="guest-dashboard-no-apps" subtitle="guest-dashboard-no-apps-subtitle" />
|
||||
) : (
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Image from 'next/image';
|
||||
import React from 'react';
|
||||
import { getLogo } from '@/lib/themes';
|
||||
import { Button } from '../ui/Button';
|
||||
|
||||
interface IProps {
|
||||
|
@ -16,7 +17,7 @@ export const StatusScreen: React.FC<IProps> = ({ title, subtitle, onAction, acti
|
|||
<Image
|
||||
alt="Tipi log"
|
||||
className="mb-3"
|
||||
src="/tipi.png"
|
||||
src={getLogo(false)}
|
||||
height={50}
|
||||
width={50}
|
||||
style={{
|
||||
|
|
|
@ -10,15 +10,16 @@ type Props = {
|
|||
children: React.ReactNode;
|
||||
title: MessageKey;
|
||||
subtitle?: MessageKey;
|
||||
autoTheme: boolean;
|
||||
};
|
||||
|
||||
export const UnauthenticatedPage = (props: Props) => {
|
||||
const { children, title, subtitle } = props;
|
||||
const { children, title, subtitle, autoTheme } = props;
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<Header authenticated={false} />
|
||||
<Header authenticated={false} autoTheme={autoTheme} />
|
||||
<div className="page-wrapper">
|
||||
<div className="page-header d-print-none">
|
||||
<div className="container-xl">
|
||||
|
|
|
@ -251,6 +251,8 @@
|
|||
"invalid-domain": "Invalid domain",
|
||||
"guest-dashboard": "Enable guest dashboard",
|
||||
"guest-dashboard-hint": "This will allow non-authenticated users to see a limited dashboard and easily access the running apps on your instance.",
|
||||
"allow-auto-themes": "Allow auto themes",
|
||||
"allow-auto-themes-hint": "Be surprised by themes that change automatically based on the time of the year.",
|
||||
"domain-name": "Domain name",
|
||||
"domain-name-hint": "Make sure this exact domain contains an A record pointing to your IP.",
|
||||
"dns-ip": "DNS IP",
|
||||
|
|
38
src/lib/themes.ts
Normal file
38
src/lib/themes.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
export const THEMES = {
|
||||
christmas: {
|
||||
name: 'christmas',
|
||||
month: 11,
|
||||
day: 1,
|
||||
durationInDays: 26,
|
||||
},
|
||||
};
|
||||
|
||||
export type Theme = keyof typeof THEMES | 'default';
|
||||
|
||||
export const getAutoTheme = (): Theme => {
|
||||
const date = new Date();
|
||||
|
||||
const theme = Object.entries(THEMES).find(([, { month, day, durationInDays }]) => {
|
||||
const startDate = new Date(date.getFullYear(), month, day);
|
||||
const endDate = new Date(date.getFullYear(), month, day + durationInDays);
|
||||
|
||||
return startDate <= date && date <= endDate;
|
||||
});
|
||||
|
||||
return theme ? (theme[0] as Theme) : 'default';
|
||||
};
|
||||
|
||||
export const getLogo = (autoTheme: boolean) => {
|
||||
if (!autoTheme) {
|
||||
return '/tipi.png';
|
||||
}
|
||||
|
||||
const theme = getAutoTheme();
|
||||
|
||||
switch (theme) {
|
||||
case 'christmas':
|
||||
return '/tipi-christmas.png';
|
||||
default:
|
||||
return '/tipi.png';
|
||||
}
|
||||
};
|
|
@ -52,6 +52,7 @@ export class TipiConfig {
|
|||
demoMode: conf.DEMO_MODE,
|
||||
guestDashboard: conf.GUEST_DASHBOARD,
|
||||
seePreReleaseVersions: false,
|
||||
allowAutoThemes: true,
|
||||
};
|
||||
|
||||
const parsedConfig = envSchema.safeParse({ ...envConfig, ...this.getFileConfig() });
|
||||
|
|
|
@ -477,7 +477,7 @@ describe('Update app', () => {
|
|||
it('Should correctly update app', async () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig({});
|
||||
await insertApp({ version: 12, config: { TEST_FIELD: 'test' } }, appConfig, db);
|
||||
await insertApp({ version: 12, status: 'running', config: { TEST_FIELD: 'test' } }, appConfig, db);
|
||||
|
||||
// act
|
||||
await updateApp(appConfig.id, { version: 0 }, db);
|
||||
|
@ -487,7 +487,7 @@ describe('Update app', () => {
|
|||
expect(app).toBeDefined();
|
||||
expect(app?.config).toStrictEqual({ TEST_FIELD: 'test' });
|
||||
expect(app?.version).toBe(appConfig.tipi_version);
|
||||
expect(app?.status).toBe('stopped');
|
||||
expect(app?.status).toBe('running');
|
||||
});
|
||||
|
||||
it("Should throw if app doesn't exist", async () => {
|
||||
|
@ -505,6 +505,16 @@ describe('Update app', () => {
|
|||
const app = await getAppById(appConfig.id, db);
|
||||
expect(app?.status).toBe('stopped');
|
||||
});
|
||||
it('Should comme back to the previous status before the update of the app', async () => {
|
||||
// arrange
|
||||
const appConfig = createAppConfig({});
|
||||
await insertApp({ status: 'stopped' }, appConfig, db);
|
||||
|
||||
// act & assert
|
||||
await updateApp(appConfig.id, { version: 0 }, db);
|
||||
const app = await getAppById(appConfig.id, db);
|
||||
expect(app?.status).toBe('stopped');
|
||||
});
|
||||
});
|
||||
|
||||
describe('installedApps', () => {
|
||||
|
|
|
@ -341,6 +341,7 @@ export class AppServiceClass {
|
|||
*/
|
||||
public updateApp = async (id: string) => {
|
||||
const app = await this.queries.getApp(id);
|
||||
const appStatusBeforeUpdate = app?.status;
|
||||
|
||||
if (!app) {
|
||||
throw new TranslatedError('server-messages.errors.app-not-found', { id });
|
||||
|
@ -355,14 +356,19 @@ export class AppServiceClass {
|
|||
if (success) {
|
||||
const appInfo = getAppInfo(app.id, app.status);
|
||||
|
||||
await this.queries.updateApp(id, { status: 'running', version: appInfo?.tipi_version });
|
||||
await this.queries.updateApp(id, { version: appInfo?.tipi_version });
|
||||
if (appStatusBeforeUpdate === 'running') {
|
||||
await this.startApp(id);
|
||||
} else {
|
||||
await this.queries.updateApp(id, { status: appStatusBeforeUpdate });
|
||||
}
|
||||
} else {
|
||||
await this.queries.updateApp(id, { status: 'stopped' });
|
||||
Logger.error(`Failed to update app ${id}: ${stdout}`);
|
||||
throw new TranslatedError('server-messages.errors.app-failed-to-update', { id });
|
||||
}
|
||||
|
||||
const updatedApp = await this.queries.updateApp(id, { status: 'stopped' });
|
||||
const updatedApp = await this.getApp(id);
|
||||
return updatedApp;
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue