Compare commits

..

339 commits

Author SHA1 Message Date
allcontributors[bot]
c962f030e3 docs: update .all-contributorsrc [skip ci] 2023-12-07 08:15:30 +01:00
allcontributors[bot]
1f5701120f docs: update README.md [skip ci] 2023-12-07 08:15:30 +01:00
dependabot[bot]
249103f0a6 chore(deps-dev): bump @types/jest from 29.5.7 to 29.5.11
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 29.5.7 to 29.5.11.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

---
updated-dependencies:
- dependency-name: "@types/jest"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-06 21:04:50 +01:00
dependabot[bot]
bb1f343ac9 chore(deps): bump react-tooltip from 5.22.0 to 5.25.0
Bumps [react-tooltip](https://github.com/ReactTooltip/react-tooltip) from 5.22.0 to 5.25.0.
- [Release notes](https://github.com/ReactTooltip/react-tooltip/releases)
- [Changelog](https://github.com/ReactTooltip/react-tooltip/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ReactTooltip/react-tooltip/compare/v5.22.0...v5.25.0)

---
updated-dependencies:
- dependency-name: react-tooltip
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-06 20:50:02 +01:00
Stavros
6b371de137 refactor(ui): change font to geist 2023-12-06 20:44:51 +01:00
Nicolas Meienberger
7133a2a2dc Merge branch 'master' into develop 2023-12-01 09:12:31 +01:00
Nicolas Meienberger
44d40b4a34 Merge branch 'hotfix/restart-failure' 2023-12-01 09:07:26 +01:00
Nicolas Meienberger
03e2604ca0 hotfix(docker): make postgres restart policy "unless-stopped" instead of "on-failure" 2023-12-01 08:44:53 +01:00
Nicolas Meienberger
ba3d860176 hotfix(install.sh): ask to re-run install script after docker is installed 2023-11-29 18:34:50 +01:00
Nicolas Meienberger
250e78450f chore: bump node to 20 in dockerfiles and actions 2023-11-29 08:41:35 +01:00
dependabot[bot]
d11299eeb8 chore(deps-dev): bump @types/cli-progress from 3.11.4 to 3.11.5
Bumps [@types/cli-progress](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/cli-progress) from 3.11.4 to 3.11.5.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/cli-progress)

---
updated-dependencies:
- dependency-name: "@types/cli-progress"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-29 08:22:40 +01:00
dependabot[bot]
6cd7ca1a4e chore(deps): bump @tabler/icons-react from 2.40.0 to 2.42.0
Bumps [@tabler/icons-react](https://github.com/tabler/tabler-icons/tree/HEAD/packages/icons-react) from 2.40.0 to 2.42.0.
- [Release notes](https://github.com/tabler/tabler-icons/releases)
- [Commits](https://github.com/tabler/tabler-icons/commits/v2.42.0/packages/icons-react)

---
updated-dependencies:
- dependency-name: "@tabler/icons-react"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-29 08:22:22 +01:00
dependabot[bot]
bb9f26b2b1 chore(deps-dev): bump @types/react from 18.2.34 to 18.2.39
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 18.2.34 to 18.2.39.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-29 08:22:09 +01:00
dependabot[bot]
30890bedd5 chore(deps-dev): bump eslint-config-next from 14.0.1 to 14.0.3
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 14.0.1 to 14.0.3.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v14.0.3/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-29 08:21:58 +01:00
dependabot[bot]
3876e8ee55 chore(deps-dev): bump @typescript-eslint/eslint-plugin
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.9.1 to 6.13.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.13.1/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-29 08:21:44 +01:00
Nicolas Meienberger
92d5a7b6a3 ci(release): make publish dependant on all builds 2023-11-28 22:31:44 +01:00
Nicolas Meienberger
ca3dc64fd4 ci(release): fix dependency on first job 2023-11-28 22:30:30 +01:00
Nicolas Meienberger
59dbe672c8
Merge pull request #960 from runtipi/release/2.2.0
Release 2.2.0
2023-11-28 22:29:01 +01:00
Nicolas Meienberger
42349c5a27 fix(worker): no need to mount host 2023-11-28 22:02:52 +01:00
Nicolas Meienberger
8104a9f3f7 ci(e2e): update upgrade before running install script 2023-11-28 19:12:40 +01:00
Nicolas Meienberger
83e96cfd31 ci(releases): refactor to use non-outdated actions 2023-11-28 19:12:35 +01:00
Nicolas Meienberger
4d69fc4cff ci(e2e): update upgrade before running install script 2023-11-28 18:49:47 +01:00
Nicolas Meienberger
37c551da1f ci(releases): refactor to use non-outdated actions 2023-11-28 13:29:02 +01:00
Nicolas Meienberger
852128f551 fix(worker): default to disk 0 if /host/root not found
In order to avoid displaying 0 disk space on some systems, we default to the first disk found
2023-11-28 08:26:43 +01:00
Nicolas Meienberger
bffa31c0b7 fix(worker): default to disk 0 if /host/root not found
In order to avoid displaying 0 disk space on some systems, we default to the first disk found
2023-11-28 08:18:48 +01:00
Nicolas Meienberger
0f129a7809 Merge branch 'steveiliop56-develop' into develop 2023-11-27 21:07:51 +01:00
Nicolas Meienberger
79b448adf3 fix(worker): apply file permissions on start 2023-11-27 21:06:14 +01:00
Nicolas Meienberger
dffa3ed670 ci(beta): build arm64 images 2023-11-27 21:06:14 +01:00
allcontributors[bot]
ef1ac3633b docs: update .all-contributorsrc [skip ci] 2023-11-27 21:06:14 +01:00
allcontributors[bot]
e520afdeff docs: update README.md [skip ci] 2023-11-27 21:06:14 +01:00
Olivier Garcia
52499cb0bd feat(support-repoURL-with-branch-syntax): If a appstore repo URL contains a branch, checkout that branch 2023-11-27 21:06:14 +01:00
Nicolas Meienberger
cfeb9d4e19 fix(app-status): rely on server status after update 2023-11-27 21:06:14 +01:00
Nicolas Meienberger
a1515ac7b8 ci(beta): adapt workflow with new worker build 2023-11-27 21:06:14 +01:00
Nicolas Meienberger
396d08dde0 feat(ThemeProvider): add some magic 2023-11-27 21:06:14 +01:00
Nicolas Meienberger
3c7dcfeb5e ci(release): replace butlerlogic/action-autotag with Klemensas/action-autotag 2023-11-27 21:06:14 +01:00
dependabot[bot]
9b5bcc7147 chore(deps-dev): bump @types/web-push from 3.6.2 to 3.6.3
Bumps [@types/web-push](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/web-push) from 3.6.2 to 3.6.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/web-push)

---
updated-dependencies:
- dependency-name: "@types/web-push"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-27 21:05:46 +01:00
cchalopin
5c276bd235 feat(update app): fix tests 2023-11-27 21:05:34 +01:00
cchalopin
c1723bb36b feat(update app): change pnpm 2023-11-27 21:05:34 +01:00
cchalopin
31c3de78b8 feat(update app): back to prev status before update 2023-11-27 21:05:12 +01:00
Nicolas Meienberger
197f6e3998 fix(worker): ensure state folder is rwx for non-root users 2023-11-27 21:04:48 +01:00
Nicolas Meienberger
abff9a4d5a fix(worker): apply file permissions on start 2023-11-27 09:41:42 +01:00
Nicolas Meienberger
91a361add1 ci(beta): build arm64 images 2023-11-27 09:41:37 +01:00
allcontributors[bot]
68b3e4e8bd docs: update .all-contributorsrc [skip ci] 2023-11-27 08:21:57 +01:00
allcontributors[bot]
f893cf482d docs: update README.md [skip ci] 2023-11-27 08:21:57 +01:00
Olivier Garcia
88878fccda feat(support-repoURL-with-branch-syntax): If a appstore repo URL contains a branch, checkout that branch 2023-11-27 08:19:22 +01:00
Nicolas Meienberger
64325150d5 fix(app-status): rely on server status after update 2023-11-27 08:10:29 +01:00
Nicolas Meienberger
203db0160a Merge branch 'cchalop1-feat/restart-app-after-update' into develop 2023-11-27 07:42:43 +01:00
Nicolas Meienberger
0261cf577c fix(worker): ensure state folder is rwx for non-root users 2023-11-27 07:42:36 +01:00
Nicolas Meienberger
166d9b49f2 ci(beta): adapt workflow with new worker build 2023-11-27 07:42:36 +01:00
Nicolas Meienberger
213b9ed482 feat(ThemeProvider): add some magic 2023-11-27 07:42:36 +01:00
Nicolas Meienberger
de3141e1f8 ci(release): replace butlerlogic/action-autotag with Klemensas/action-autotag 2023-11-27 07:42:36 +01:00
dependabot[bot]
6ec79ac1e8 chore(deps-dev): bump @types/web-push from 3.6.2 to 3.6.3
Bumps [@types/web-push](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/web-push) from 3.6.2 to 3.6.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/web-push)

---
updated-dependencies:
- dependency-name: "@types/web-push"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-27 07:42:36 +01:00
Nicolas Meienberger
5dbc996b92 refactor(docker): use non-root user in dashboard image 2023-11-27 07:42:36 +01:00
Nicolas Meienberger
f76af65212 fix(worker): ensure state folder is rwx for non-root users 2023-11-27 07:42:16 +01:00
cchalopin
1357efa0e3 feat(update app): fix tests 2023-11-26 15:17:27 +01:00
cchalopin
4da6193ae9 feat(update app): change pnpm 2023-11-26 15:12:09 +01:00
cchalopin
f61e6b2dae feat(update app): back to prev status before update 2023-11-26 15:03:21 +01:00
Stavros
230ae0a412 fix(worker): remount / to /host/root 2023-11-26 11:17:11 +02:00
Nicolas Meienberger
7c967271d7 ci(beta): adapt workflow with new worker build 2023-11-26 09:30:19 +01:00
Nicolas Meienberger
3d85051915 feat(ThemeProvider): add some magic 2023-11-26 09:13:06 +01:00
Nicolas Meienberger
467fc070d9 ci(release): replace butlerlogic/action-autotag with Klemensas/action-autotag 2023-11-25 11:43:18 +01:00
dependabot[bot]
14b4331d1d chore(deps-dev): bump @types/web-push from 3.6.2 to 3.6.3
Bumps [@types/web-push](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/web-push) from 3.6.2 to 3.6.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/web-push)

---
updated-dependencies:
- dependency-name: "@types/web-push"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-25 11:27:47 +01:00
Nicolas Meienberger
dc427acced refactor(docker): use non-root user in dashboard image 2023-11-25 11:24:08 +01:00
Stavros
769f671749 refactor(worker): no need to mount host 2023-11-23 07:47:22 +01:00
Stavros
fb861e26be fix(worker): display disk usage right 2023-11-23 07:47:22 +01:00
dependabot[bot]
ab71d75fd1 chore(deps-dev): bump knip from 2.39.0 to 2.41.3
Bumps [knip](https://github.com/webpro/knip) from 2.39.0 to 2.41.3.
- [Release notes](https://github.com/webpro/knip/releases)
- [Changelog](https://github.com/webpro/knip/blob/main/docs/release-notes-v2.md)
- [Commits](https://github.com/webpro/knip/compare/2.39.0...2.41.3)

---
updated-dependencies:
- dependency-name: knip
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-20 19:41:54 +01:00
dependabot[bot]
8743422741 chore(deps-dev): bump @types/fs-extra from 11.0.3 to 11.0.4
Bumps [@types/fs-extra](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/fs-extra) from 11.0.3 to 11.0.4.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/fs-extra)

---
updated-dependencies:
- dependency-name: "@types/fs-extra"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-20 19:41:42 +01:00
Nicolas Meienberger
47e19b967f fix(docker-compose): mount host at /mnt/host 2023-11-20 19:38:30 +01:00
Nicolas Meienberger
f43bec37bb Merge branch 'steveiliop56-disk-usage-fix' into develop 2023-11-20 19:33:06 +01:00
Nicolas Meienberger
0fe29a9a80 Merge branch 'disk-usage-fix' of https://github.com/steveiliop56/runtipi into steveiliop56-disk-usage-fix 2023-11-20 19:32:49 +01:00
Stavros
0f1c7d4ee5 fix(app-update): change confirmation text 2023-11-20 19:28:28 +01:00
Stavros
bd336c652e fix(worker): display disk usage right 2023-11-20 19:54:11 +02:00
dependabot[bot]
c1ac3060f2 chore(deps): bump argon2 from 0.31.1 to 0.31.2
Bumps [argon2](https://github.com/ranisalt/node-argon2) from 0.31.1 to 0.31.2.
- [Release notes](https://github.com/ranisalt/node-argon2/releases)
- [Commits](https://github.com/ranisalt/node-argon2/compare/v0.31.1...v0.31.2)

---
updated-dependencies:
- dependency-name: argon2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-20 18:53:29 +01:00
dependabot[bot]
bbe0c961cc chore(deps): bump actions/github-script from 6 to 7
Bumps [actions/github-script](https://github.com/actions/github-script) from 6 to 7.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-20 18:53:02 +01:00
Nicolas Meienberger
c18b00e78b refactor(docker-compose): change container to restart policy "unless-stopped" 2023-11-20 18:52:04 +01:00
Nicolas Meienberger
60e28fd6b2 refactor: download docker binary instead of installing with apk to reduce size of worker image 2023-11-20 18:52:04 +01:00
Stavros
2ef93c50eb fix(app): remove the file extension from folder 2023-11-20 18:43:33 +01:00
Nicolas Meienberger
ef541f1d77 fix(config): always use internal port 5432 for postgres in non-test mode 2023-11-19 17:35:53 +01:00
allcontributors[bot]
3dd7ef7cdd docs: update .all-contributorsrc [skip ci] 2023-11-18 10:36:11 +01:00
allcontributors[bot]
922b7722f1 docs: update README.md [skip ci] 2023-11-18 10:36:11 +01:00
allcontributors[bot]
ce0bc75046 docs: update .all-contributorsrc [skip ci] 2023-11-18 10:35:32 +01:00
allcontributors[bot]
7d7cee3405 docs: update README.md [skip ci] 2023-11-18 10:35:32 +01:00
Nicolas Meienberger
56acfbbb64 Merge branch 'cchalop1-feat/reset-app' into develop 2023-11-18 10:25:27 +01:00
Nicolas Meienberger
e6d838f3d1 refactor(reset-app): simplify status management 2023-11-18 10:25:15 +01:00
cchalopin
3bfb015b13 feat(reset-app): fix reset wording 2023-11-18 10:25:15 +01:00
cchalopin
dd32bd631f feat(reset-app): adding a reset app from setting modal 2023-11-18 10:25:15 +01:00
Nicolas Meienberger
8bfafac7bd fix: reload env variables before loading config
fix: read env
2023-11-16 20:49:27 +01:00
Nicolas Meienberger
e0d52e79c1 fix(cli): ensure user is allowed to run docker commands before starting 2023-11-16 20:49:27 +01:00
Nicolas Meienberger
2dcb358392 chore: bump version to 2.2.0 2023-11-16 20:49:27 +01:00
Nicolas Meienberger
9efacb5797 ci(alpha): add image build for worker 2023-11-16 20:49:27 +01:00
Nicolas Meienberger
8e19c8b0e8 refactor: remove restart option from UI 2023-11-16 20:49:27 +01:00
Nicolas Meienberger
bdfb019df2 fix(worker): docker not throwing an error when failing to start 2023-11-16 20:49:27 +01:00
Nicolas Meienberger
af8509aacc test(worker): implement test suites with vitest 2023-11-16 20:49:27 +01:00
Nicolas Meienberger
55e0cd155e refactor(shared): factorize common helpers in shared package 2023-11-16 20:49:27 +01:00
Nicolas Meienberger
417785ee24 refactor(worker): move executors from cli to worker 2023-11-16 20:49:27 +01:00
Nicolas Meienberger
5f18ff16de feat: create base worker package 2023-11-16 20:49:27 +01:00
Stavros
09461527ee
feat(cli): set custom postgres port in config.json (#918)
* feat(cli): set custom postgres port in config.json

* fix(cli): small misspelling error.

* fix(cli): misspelling error again

Co-authored-by: Nicolas Meienberger <47644445+meienberger@users.noreply.github.com>

* fix(cli): add required env vard to schemas.

* fix(cli): add default port to schema

* fix(cli): add requested changes

* fix(cli): move posgresPort to pick

---------

Co-authored-by: Nicolas Meienberger <47644445+meienberger@users.noreply.github.com>
2023-11-15 21:16:10 +01:00
Nicolas Meienberger
ff6c90849e ci: enable ci runs on pull_request 2023-11-14 19:24:56 +01:00
Nicolas Meienberger
d032bbdc5f chore(cli): remove duplicate stopping app messages 2023-11-07 22:44:52 +01:00
Nicolas Meienberger
df59d21ce7
Merge pull request #879 from runtipi/release/2.1.0
Release 2.1.0
2023-11-07 22:14:38 +01:00
Nicolas Meienberger
f38c3416a4 chore(cli): remove duplicate starting app message 2023-11-07 21:58:49 +01:00
dependabot[bot]
ff9bf2760b chore(deps-dev): bump @typescript-eslint/parser from 6.9.1 to 6.10.0
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.9.1 to 6.10.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.10.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-07 07:29:45 +01:00
dependabot[bot]
87c891d760 chore(deps): bump react-select from 5.7.7 to 5.8.0
Bumps [react-select](https://github.com/JedWatson/react-select) from 5.7.7 to 5.8.0.
- [Release notes](https://github.com/JedWatson/react-select/releases)
- [Changelog](https://github.com/JedWatson/react-select/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/JedWatson/react-select/compare/react-select@5.7.7...react-select@5.8.0)

---
updated-dependencies:
- dependency-name: react-select
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-07 07:29:00 +01:00
dependabot[bot]
566f4f0d1e chore(deps): bump react-hook-form from 7.47.0 to 7.48.2
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.47.0 to 7.48.2.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.47.0...v7.48.2)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-07 07:28:31 +01:00
dependabot[bot]
e81e26fb07 chore(deps): bump bullmq from 4.12.7 to 4.13.0
Bumps [bullmq](https://github.com/taskforcesh/bullmq) from 4.12.7 to 4.13.0.
- [Release notes](https://github.com/taskforcesh/bullmq/releases)
- [Commits](https://github.com/taskforcesh/bullmq/compare/v4.12.7...v4.13.0)

---
updated-dependencies:
- dependency-name: bullmq
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-07 07:27:57 +01:00
Nicolas Meienberger
bb7bd60be0 Merge branch 'master' of github.com:runtipi/runtipi into develop 2023-11-06 07:44:56 +01:00
Nicolas Meienberger
d823eba976 ci(release): add latest tag 2023-11-06 07:25:46 +01:00
Nicolas Meienberger
45e71539db ci: move all workflow to use ghcr.io registry 2023-11-06 07:25:46 +01:00
Nicolas Meienberger
47fb287bd5 ci(alpha): move from docker hub to ghcr.io registry 2023-11-06 07:25:46 +01:00
dependabot[bot]
871d72e3b4 chore(deps-dev): bump @types/react from 18.2.33 to 18.2.34
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 18.2.33 to 18.2.34.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-03 22:48:35 +01:00
dependabot[bot]
fa35eb3509 chore(deps): bump next-intl from 2.21.0 to 2.22.1
Bumps [next-intl](https://github.com/amannn/next-intl) from 2.21.0 to 2.22.1.
- [Changelog](https://github.com/amannn/next-intl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amannn/next-intl/compare/v2.21.0...v2.22.1)

---
updated-dependencies:
- dependency-name: next-intl
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-03 22:14:13 +01:00
dependabot[bot]
275b60c363 chore(deps-dev): bump eslint-config-next from 14.0.0 to 14.0.1
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 14.0.0 to 14.0.1.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v14.0.1/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-03 22:13:46 +01:00
dependabot[bot]
1333414ca1 chore(deps-dev): bump @vitejs/plugin-react from 4.1.0 to 4.1.1
Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/v4.1.1/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-03 22:13:01 +01:00
dependabot[bot]
92238dd8c9 chore(deps-dev): bump eslint-plugin-jsx-a11y from 6.7.1 to 6.8.0
Bumps [eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y) from 6.7.1 to 6.8.0.
- [Release notes](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/releases)
- [Changelog](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/compare/v6.7.1...v6.8.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-jsx-a11y
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-03 22:12:46 +01:00
dependabot[bot]
5b743f2d0a chore(deps): bump react-tooltip from 5.21.6 to 5.22.0
Bumps [react-tooltip](https://github.com/ReactTooltip/react-tooltip) from 5.21.6 to 5.22.0.
- [Release notes](https://github.com/ReactTooltip/react-tooltip/releases)
- [Changelog](https://github.com/ReactTooltip/react-tooltip/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ReactTooltip/react-tooltip/compare/v5.21.6...v5.22.0)

---
updated-dependencies:
- dependency-name: react-tooltip
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-03 22:12:33 +01:00
dependabot[bot]
c18a1d87e1 chore(deps-dev): bump knip from 2.38.4 to 2.39.0
Bumps [knip](https://github.com/webpro/knip) from 2.38.4 to 2.39.0.
- [Release notes](https://github.com/webpro/knip/releases)
- [Changelog](https://github.com/webpro/knip/blob/main/docs/release-notes-v2.md)
- [Commits](https://github.com/webpro/knip/compare/2.38.4...2.39.0)

---
updated-dependencies:
- dependency-name: knip
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-03 22:11:55 +01:00
dependabot[bot]
15130cbe5a chore(deps): bump zustand from 4.4.4 to 4.4.6
Bumps [zustand](https://github.com/pmndrs/zustand) from 4.4.4 to 4.4.6.
- [Release notes](https://github.com/pmndrs/zustand/releases)
- [Commits](https://github.com/pmndrs/zustand/compare/v4.4.4...v4.4.6)

---
updated-dependencies:
- dependency-name: zustand
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-03 22:11:35 +01:00
Nicolas Meienberger
1296c6c3ce test(e2e): add suite for changing email 2023-11-03 21:49:46 +01:00
Nicolas Meienberger
49c6a8f9b7 feat(client): add form for change email in settings page 2023-11-03 21:49:46 +01:00
Nicolas Meienberger
955b4ccc18 feat(auth.service): add service and action for update email 2023-11-03 21:49:46 +01:00
Nicolas Meienberger
8c0956aa56 chore(deps): bump next to v14 2023-11-03 21:49:46 +01:00
Nicolas Meienberger
cea9697b6d fix: downgrade log-update to version 5.0.1 2023-11-02 20:21:16 +01:00
Nicolas Meienberger
677fb12ad4 chore: bump version to 2.1.0 2023-11-02 19:18:59 +01:00
dependabot[bot]
e6ffa7df12 chore(deps-dev): bump @typescript-eslint/parser from 6.9.0 to 6.9.1
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.9.0 to 6.9.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.9.1/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-02 19:14:41 +01:00
dependabot[bot]
eccc1decad chore(deps-dev): bump @types/node from 20.8.9 to 20.8.10
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.8.9 to 20.8.10.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-02 19:14:25 +01:00
dependabot[bot]
822c979458 chore(deps): bump next-safe-action from 4.0.4 to 4.0.5
Bumps [next-safe-action](https://github.com/TheEdoRan/next-safe-action) from 4.0.4 to 4.0.5.
- [Release notes](https://github.com/TheEdoRan/next-safe-action/releases)
- [Changelog](https://github.com/TheEdoRan/next-safe-action/blob/main/.releaserc.json)
- [Commits](https://github.com/TheEdoRan/next-safe-action/compare/v4.0.4...v4.0.5)

---
updated-dependencies:
- dependency-name: next-safe-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-02 19:14:15 +01:00
Nicolas Meienberger
79153b6b78 Update install.sh 2023-11-02 19:12:59 +01:00
Nicolas Meienberger
0eb7218063 fix(markdown): remove custom image component 2023-11-02 19:10:56 +01:00
dependabot[bot]
a2d9610c22 chore(deps): bump bullmq from 4.12.6 to 4.12.7
Bumps [bullmq](https://github.com/taskforcesh/bullmq) from 4.12.6 to 4.12.7.
- [Release notes](https://github.com/taskforcesh/bullmq/releases)
- [Commits](https://github.com/taskforcesh/bullmq/compare/v4.12.6...v4.12.7)

---
updated-dependencies:
- dependency-name: bullmq
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-02 19:07:59 +01:00
dependabot[bot]
c4d25792ad chore(deps-dev): bump @typescript-eslint/eslint-plugin
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.9.0 to 6.9.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.9.1/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-02 19:06:27 +01:00
dependabot[bot]
9c4597da8e chore(deps-dev): bump @types/jest from 29.5.6 to 29.5.7
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 29.5.6 to 29.5.7.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

---
updated-dependencies:
- dependency-name: "@types/jest"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-02 19:05:57 +01:00
dependabot[bot]
1d6228a96a chore(deps): bump log-update from 5.0.1 to 6.0.0
Bumps [log-update](https://github.com/sindresorhus/log-update) from 5.0.1 to 6.0.0.
- [Release notes](https://github.com/sindresorhus/log-update/releases)
- [Commits](https://github.com/sindresorhus/log-update/compare/v5.0.1...v6.0.0)

---
updated-dependencies:
- dependency-name: log-update
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-02 19:05:29 +01:00
dependabot[bot]
8af4028027 chore(deps): bump next-client-cookies from 1.0.5 to 1.0.6
Bumps [next-client-cookies](https://github.com/moshest/next-client-cookies) from 1.0.5 to 1.0.6.
- [Commits](https://github.com/moshest/next-client-cookies/compare/v1.0.5...v1.0.6)

---
updated-dependencies:
- dependency-name: next-client-cookies
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-02 19:05:15 +01:00
Nicolas Meienberger
3ddcf3732e test(e2e): add test suite for guest dashboard 2023-11-02 19:04:57 +01:00
Nicolas Meienberger
ab3dcabbf9 feat: create new page for guest dashboard 2023-11-02 19:04:57 +01:00
Nicolas Meienberger
c0723257e6 feat(settings): add enable guest dashboard toggle 2023-11-02 19:04:57 +01:00
Nicolas Meienberger
5830d16382 feat(apps): add "enable on guest dashboard option" 2023-11-02 19:04:57 +01:00
Nicolas Meienberger
fd6c5afe2c chore(cli): add guest dashboard env variable 2023-11-02 19:04:57 +01:00
Nicolas Meienberger
9b9541cee7 chore: create migration to add is_visible_on_guest_dahsboard field on app table 2023-11-02 19:04:57 +01:00
Nicolas Meienberger
bf8f635680
Update install.sh 2023-11-02 18:01:09 +01:00
dependabot[bot]
9688e059f3 chore(deps): bump @tabler/icons-react from 2.39.0 to 2.40.0
Bumps [@tabler/icons-react](https://github.com/tabler/tabler-icons/tree/HEAD/packages/icons-react) from 2.39.0 to 2.40.0.
- [Release notes](https://github.com/tabler/tabler-icons/releases)
- [Commits](https://github.com/tabler/tabler-icons/commits/v2.40.0/packages/icons-react)

---
updated-dependencies:
- dependency-name: "@tabler/icons-react"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-28 12:15:38 +02:00
dependabot[bot]
6c0f7548f7 chore(deps): bump axios from 1.5.1 to 1.6.0
Bumps [axios](https://github.com/axios/axios) from 1.5.1 to 1.6.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.5.1...v1.6.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-28 12:15:11 +02:00
dependabot[bot]
e8b362914a chore(deps-dev): bump knip from 2.38.1 to 2.38.4
Bumps [knip](https://github.com/webpro/knip) from 2.38.1 to 2.38.4.
- [Release notes](https://github.com/webpro/knip/releases)
- [Changelog](https://github.com/webpro/knip/blob/main/docs/release-notes-v2.md)
- [Commits](https://github.com/webpro/knip/compare/2.38.1...2.38.4)

---
updated-dependencies:
- dependency-name: knip
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-28 12:14:43 +02:00
dependabot[bot]
a4af320fba chore(deps-dev): bump eslint-plugin-jest from 27.5.0 to 27.6.0
Bumps [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) from 27.5.0 to 27.6.0.
- [Release notes](https://github.com/jest-community/eslint-plugin-jest/releases)
- [Changelog](https://github.com/jest-community/eslint-plugin-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jest-community/eslint-plugin-jest/compare/v27.5.0...v27.6.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-jest
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-28 12:14:19 +02:00
dependabot[bot]
8b221dcf0a chore(deps): bump systeminformation from 5.21.13 to 5.21.15
Bumps [systeminformation](https://github.com/sebhildebrandt/systeminformation) from 5.21.13 to 5.21.15.
- [Changelog](https://github.com/sebhildebrandt/systeminformation/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sebhildebrandt/systeminformation/compare/v5.21.13...v5.21.15)

---
updated-dependencies:
- dependency-name: systeminformation
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-28 12:13:48 +02:00
dependabot[bot]
5b7615a8ee chore(deps-dev): bump eslint from 8.51.0 to 8.52.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.51.0 to 8.52.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.51.0...v8.52.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-27 12:26:54 +02:00
dependabot[bot]
e06e946376 chore(deps-dev): bump @types/react from 18.2.31 to 18.2.33
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 18.2.31 to 18.2.33.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-27 12:26:38 +02:00
dependabot[bot]
a9d8f2e35f chore(deps-dev): bump eslint-config-next from 13.5.6 to 14.0.0
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 13.5.6 to 14.0.0.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v14.0.0/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-27 10:10:49 +02:00
dependabot[bot]
d93bf2cb7c chore(deps-dev): bump @types/semver from 7.5.3 to 7.5.4
Bumps [@types/semver](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/semver) from 7.5.3 to 7.5.4.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/semver)

---
updated-dependencies:
- dependency-name: "@types/semver"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-27 10:10:03 +02:00
dependabot[bot]
01062d0041 chore(deps-dev): bump @types/node from 20.8.4 to 20.8.9
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.8.4 to 20.8.9.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-27 10:09:20 +02:00
dependabot[bot]
28811c5429 chore(deps-dev): bump tsx from 3.13.0 to 3.14.0
Bumps [tsx](https://github.com/esbuild-kit/tsx) from 3.13.0 to 3.14.0.
- [Release notes](https://github.com/esbuild-kit/tsx/releases)
- [Changelog](https://github.com/esbuild-kit/tsx/blob/develop/release.config.cjs)
- [Commits](https://github.com/esbuild-kit/tsx/compare/v3.13.0...v3.14.0)

---
updated-dependencies:
- dependency-name: tsx
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-27 09:58:00 +02:00
dependabot[bot]
bb887f284e chore(deps): bump bullmq from 4.12.5 to 4.12.6
Bumps [bullmq](https://github.com/taskforcesh/bullmq) from 4.12.5 to 4.12.6.
- [Release notes](https://github.com/taskforcesh/bullmq/releases)
- [Commits](https://github.com/taskforcesh/bullmq/compare/v4.12.5...v4.12.6)

---
updated-dependencies:
- dependency-name: bullmq
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-27 09:57:24 +02:00
dependabot[bot]
0fe2ab13ab chore(deps): bump sass from 1.69.4 to 1.69.5
Bumps [sass](https://github.com/sass/dart-sass) from 1.69.4 to 1.69.5.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.69.4...1.69.5)

---
updated-dependencies:
- dependency-name: sass
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-27 09:57:09 +02:00
dependabot[bot]
bcb1b37110 chore(deps-dev): bump eslint-plugin-jest from 27.4.3 to 27.5.0
Bumps [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) from 27.4.3 to 27.5.0.
- [Release notes](https://github.com/jest-community/eslint-plugin-jest/releases)
- [Changelog](https://github.com/jest-community/eslint-plugin-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jest-community/eslint-plugin-jest/compare/v27.4.3...v27.5.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-jest
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-27 09:56:50 +02:00
Nicolas Meienberger
6341140e9d refactor: wrap InstallForm in ScrollArea component 2023-10-26 09:00:39 +02:00
Nicolas Meienberger
ac6f54312d feat(ui): create ScrollArea component 2023-10-26 09:00:39 +02:00
Nicolas Meienberger
96a00de418 chore(packages): install @radix-ui/react-scroll-area 2023-10-26 09:00:39 +02:00
Nicolas Meienberger
2af1239c67 fix(app-details): align description text left when md or larger 2023-10-26 08:02:37 +02:00
dependabot[bot]
d2c6909959 chore(deps-dev): bump @types/validator from 13.11.2 to 13.11.5
Bumps [@types/validator](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/validator) from 13.11.2 to 13.11.5.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/validator)

---
updated-dependencies:
- dependency-name: "@types/validator"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-26 07:27:23 +02:00
dependabot[bot]
a51e26e0dc chore(deps): bump next-safe-action from 4.0.1 to 4.0.4
Bumps [next-safe-action](https://github.com/TheEdoRan/next-safe-action) from 4.0.1 to 4.0.4.
- [Release notes](https://github.com/TheEdoRan/next-safe-action/releases)
- [Commits](https://github.com/TheEdoRan/next-safe-action/compare/v4.0.1...v4.0.4)

---
updated-dependencies:
- dependency-name: next-safe-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-26 07:27:11 +02:00
dependabot[bot]
3a41c0c9c0 chore(deps): bump commander from 11.0.0 to 11.1.0
Bumps [commander](https://github.com/tj/commander.js) from 11.0.0 to 11.1.0.
- [Release notes](https://github.com/tj/commander.js/releases)
- [Changelog](https://github.com/tj/commander.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tj/commander.js/compare/v11.0.0...v11.1.0)

---
updated-dependencies:
- dependency-name: commander
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-26 07:26:36 +02:00
dependabot[bot]
17f13690a5 chore(deps-dev): bump knip from 2.33.1 to 2.38.1
Bumps [knip](https://github.com/webpro/knip) from 2.33.1 to 2.38.1.
- [Release notes](https://github.com/webpro/knip/releases)
- [Changelog](https://github.com/webpro/knip/blob/main/docs/release-notes-v2.md)
- [Commits](https://github.com/webpro/knip/compare/2.33.1...2.38.1)

---
updated-dependencies:
- dependency-name: knip
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-26 07:25:27 +02:00
dependabot[bot]
63869cc1f3 chore(deps): bump react-tooltip from 5.21.5 to 5.21.6
Bumps [react-tooltip](https://github.com/ReactTooltip/react-tooltip) from 5.21.5 to 5.21.6.
- [Release notes](https://github.com/ReactTooltip/react-tooltip/releases)
- [Changelog](https://github.com/ReactTooltip/react-tooltip/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ReactTooltip/react-tooltip/compare/v5.21.5...v5.21.6)

---
updated-dependencies:
- dependency-name: react-tooltip
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-26 07:25:09 +02:00
dependabot[bot]
d5a313fc03 chore(deps-dev): bump @types/web-push from 3.6.1 to 3.6.2
Bumps [@types/web-push](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/web-push) from 3.6.1 to 3.6.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/web-push)

---
updated-dependencies:
- dependency-name: "@types/web-push"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-26 07:24:45 +02:00
dependabot[bot]
36a683a36e chore(deps): bump bullmq from 4.12.3 to 4.12.5
Bumps [bullmq](https://github.com/taskforcesh/bullmq) from 4.12.3 to 4.12.5.
- [Release notes](https://github.com/taskforcesh/bullmq/releases)
- [Commits](https://github.com/taskforcesh/bullmq/compare/v4.12.3...v4.12.5)

---
updated-dependencies:
- dependency-name: bullmq
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-26 07:24:32 +02:00
dependabot[bot]
9a95a980ad chore(deps-dev): bump @types/fs-extra from 11.0.2 to 11.0.3
Bumps [@types/fs-extra](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/fs-extra) from 11.0.2 to 11.0.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/fs-extra)

---
updated-dependencies:
- dependency-name: "@types/fs-extra"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-26 07:24:09 +02:00
Nicolas Meienberger
5be75ad98b refactor: move 404 page to RSC 2023-10-25 18:49:45 +02:00
dependabot[bot]
f38b66c6e8 chore(deps-dev): bump @typescript-eslint/parser from 6.8.0 to 6.9.0
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.8.0 to 6.9.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.9.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-25 18:39:31 +02:00
Nicolas Meienberger
f39232e1a7 refactor(cli): add spinner text when stopping and starting apps 2023-10-25 18:36:11 +02:00
Nicolas Meienberger
435ff66522 refactor(cli): use flush method for cleanLogs 2023-10-25 18:36:11 +02:00
Nicolas Meienberger
ef0298665c refactor(cli): improve help message for app commands 2023-10-25 18:36:11 +02:00
dependabot[bot]
302f819d02 chore(deps-dev): bump eslint-config-next from 13.5.4 to 13.5.6
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 13.5.4 to 13.5.6.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v13.5.6/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-24 23:08:20 +02:00
dependabot[bot]
3ebb345373 chore(deps): bump next from 13.5.5 to 13.5.6
Bumps [next](https://github.com/vercel/next.js) from 13.5.5 to 13.5.6.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v13.5.5...v13.5.6)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-24 23:07:51 +02:00
dependabot[bot]
d76a1fe8af chore(deps-dev): bump @babel/core from 7.23.0 to 7.23.2
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.23.0 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/@babel/core@7.23.2/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-24 23:00:17 +02:00
dependabot[bot]
82b9cfa96d chore(deps-dev): bump @typescript-eslint/eslint-plugin
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.8.0 to 6.9.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.9.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-24 22:59:52 +02:00
dependabot[bot]
9e1137da9e chore(deps-dev): bump @playwright/test from 1.38.1 to 1.39.0
Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.38.1 to 1.39.0.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.38.1...v1.39.0)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-24 22:59:35 +02:00
dependabot[bot]
3388fdaf88 chore(deps-dev): bump @testing-library/jest-dom from 6.1.3 to 6.1.4
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 6.1.3 to 6.1.4.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v6.1.3...v6.1.4)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-24 22:58:40 +02:00
dependabot[bot]
2cdc7e2d5b chore(deps-dev): bump @types/cli-progress from 3.11.3 to 3.11.4
Bumps [@types/cli-progress](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/cli-progress) from 3.11.3 to 3.11.4.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/cli-progress)

---
updated-dependencies:
- dependency-name: "@types/cli-progress"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-24 22:58:14 +02:00
dependabot[bot]
db1e544f12 chore(deps): bump zustand from 4.4.3 to 4.4.4
Bumps [zustand](https://github.com/pmndrs/zustand) from 4.4.3 to 4.4.4.
- [Release notes](https://github.com/pmndrs/zustand/releases)
- [Commits](https://github.com/pmndrs/zustand/compare/v4.4.3...v4.4.4)

---
updated-dependencies:
- dependency-name: zustand
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-24 22:33:58 +02:00
dependabot[bot]
0888e4a873 chore(deps-dev): bump @faker-js/faker from 8.1.0 to 8.2.0
Bumps [@faker-js/faker](https://github.com/faker-js/faker) from 8.1.0 to 8.2.0.
- [Release notes](https://github.com/faker-js/faker/releases)
- [Changelog](https://github.com/faker-js/faker/blob/next/CHANGELOG.md)
- [Commits](https://github.com/faker-js/faker/compare/v8.1.0...v8.2.0)

---
updated-dependencies:
- dependency-name: "@faker-js/faker"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-24 22:33:35 +02:00
Nicolas Meienberger
9612ff3336 fix(app-store): show a text when there is no filtering result 2023-10-24 21:27:08 +02:00
dependabot[bot]
8ab15acf84 chore(deps-dev): bump eslint-plugin-import from 2.28.1 to 2.29.0
Bumps [eslint-plugin-import](https://github.com/import-js/eslint-plugin-import) from 2.28.1 to 2.29.0.
- [Release notes](https://github.com/import-js/eslint-plugin-import/releases)
- [Changelog](https://github.com/import-js/eslint-plugin-import/blob/main/CHANGELOG.md)
- [Commits](https://github.com/import-js/eslint-plugin-import/compare/v2.28.1...v2.29.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-import
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-24 20:53:47 +02:00
dependabot[bot]
1f4b215497 chore(deps-dev): bump @types/pg from 8.10.5 to 8.10.7
Bumps [@types/pg](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/pg) from 8.10.5 to 8.10.7.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/pg)

---
updated-dependencies:
- dependency-name: "@types/pg"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-24 20:53:36 +02:00
Nicolas Meienberger
1925b0cb75 chore(cli): flush all logs when app is starting 2023-10-24 19:43:33 +02:00
dependabot[bot]
04b9c9d5e0 chore(deps-dev): bump @types/lodash.merge from 4.6.7 to 4.6.8
Bumps [@types/lodash.merge](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash.merge) from 4.6.7 to 4.6.8.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash.merge)

---
updated-dependencies:
- dependency-name: "@types/lodash.merge"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-24 19:41:50 +02:00
dependabot[bot]
8dd4c78641 chore(deps): bump actions/setup-node from 3 to 4
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 3 to 4.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-24 19:41:33 +02:00
dependabot[bot]
89fe3156a5 chore(deps-dev): bump vite from 4.4.11 to 4.5.0
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.4.11 to 4.5.0.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.0/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.0/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-24 19:40:50 +02:00
dependabot[bot]
cb77164ae6 chore(deps-dev): bump @types/uuid from 9.0.5 to 9.0.6
Bumps [@types/uuid](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/uuid) from 9.0.5 to 9.0.6.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/uuid)

---
updated-dependencies:
- dependency-name: "@types/uuid"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-24 19:40:19 +02:00
dependabot[bot]
ee92eade81 chore(deps-dev): bump @types/react-dom from 18.2.13 to 18.2.14
Bumps [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) from 18.2.13 to 18.2.14.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

---
updated-dependencies:
- dependency-name: "@types/react-dom"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-24 19:40:06 +02:00
dependabot[bot]
62b91f5d91 chore(deps): bump next-intl from 2.20.2 to 2.21.0
Bumps [next-intl](https://github.com/amannn/next-intl) from 2.20.2 to 2.21.0.
- [Changelog](https://github.com/amannn/next-intl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amannn/next-intl/compare/v2.20.2...v2.21.0)

---
updated-dependencies:
- dependency-name: next-intl
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-24 19:39:33 +02:00
dependabot[bot]
8919700245 chore(deps): bump systeminformation from 5.21.11 to 5.21.13
Bumps [systeminformation](https://github.com/sebhildebrandt/systeminformation) from 5.21.11 to 5.21.13.
- [Changelog](https://github.com/sebhildebrandt/systeminformation/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sebhildebrandt/systeminformation/compare/v5.21.11...v5.21.13)

---
updated-dependencies:
- dependency-name: systeminformation
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-24 19:39:18 +02:00
dependabot[bot]
7f9346ff80 chore(deps-dev): bump eslint-plugin-jest from 27.4.2 to 27.4.3
Bumps [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) from 27.4.2 to 27.4.3.
- [Release notes](https://github.com/jest-community/eslint-plugin-jest/releases)
- [Changelog](https://github.com/jest-community/eslint-plugin-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jest-community/eslint-plugin-jest/compare/v27.4.2...v27.4.3)

---
updated-dependencies:
- dependency-name: eslint-plugin-jest
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-24 19:39:03 +02:00
dependabot[bot]
ed127b1735 chore(deps): bump docker/login-action from 2 to 3
Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-24 19:38:23 +02:00
dependabot[bot]
92e661890e chore(deps): bump SimenB/github-actions-cpu-cores from 1 to 2
Bumps [SimenB/github-actions-cpu-cores](https://github.com/simenb/github-actions-cpu-cores) from 1 to 2.
- [Release notes](https://github.com/simenb/github-actions-cpu-cores/releases)
- [Commits](https://github.com/simenb/github-actions-cpu-cores/compare/v1...v2)

---
updated-dependencies:
- dependency-name: SimenB/github-actions-cpu-cores
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-24 19:37:47 +02:00
Nicolas Meienberger
3a6f19e8df Merge branch 'develop' of github.com:meienberger/runtipi into develop 2023-10-23 21:57:58 +02:00
Nicolas Meienberger
b85fbc3224 chore(cli): pull app store repo upon start 2023-10-23 21:57:55 +02:00
Nicolas Meienberger
5db75f1d14
Update README.md 2023-10-23 21:17:40 +02:00
Nicolas Meienberger
12cafc6a97 fix(ci): udpate octokit deprecated call to github.repos 2023-10-23 20:01:49 +02:00
Nicolas Meienberger
50f509d04e fix(ci): udpate octokit deprecated call to github.repos 2023-10-23 12:19:30 +02:00
Nicolas Meienberger
d5766070c4
Merge pull request #798 from runtipi/release/2.0.7
Release/2.0.7
2023-10-23 08:51:21 +02:00
Nicolas Meienberger
3972340fe7 chore: make theme cookie expire after 365 days 2023-10-23 08:48:44 +02:00
Nicolas Meienberger
b18bf2a265 chore: make theme cookie expire after 365 days 2023-10-23 08:47:36 +02:00
Nicolas Meienberger
a2e46a84bb chore: bump version to 2.0.7 2023-10-23 07:56:24 +02:00
Nicolas Meienberger
1dd64af4ed chore(cli): generate APP_PROTOCOL env var even for http 2023-10-23 07:52:14 +02:00
Nicolas Meienberger
9f1e09098a feat(cli): add commands for starting and stopping apps 2023-10-23 07:46:37 +02:00
dependabot[bot]
b1cd37ddba chore(deps): bump sass from 1.69.2 to 1.69.4
Bumps [sass](https://github.com/sass/dart-sass) from 1.69.2 to 1.69.4.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.69.2...1.69.4)

---
updated-dependencies:
- dependency-name: sass
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-21 14:08:53 +02:00
dependabot[bot]
80550b1de7 chore(deps-dev): bump @typescript-eslint/parser from 6.7.5 to 6.8.0
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.7.5 to 6.8.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.8.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-21 14:07:50 +02:00
dependabot[bot]
7b62c1b1ee chore(deps-dev): bump @types/jest from 29.5.5 to 29.5.6
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 29.5.5 to 29.5.6.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

---
updated-dependencies:
- dependency-name: "@types/jest"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-21 14:06:44 +02:00
dependabot[bot]
28217d1f5a chore(deps-dev): bump eslint-plugin-testing-library from 6.0.2 to 6.1.0
Bumps [eslint-plugin-testing-library](https://github.com/testing-library/eslint-plugin-testing-library) from 6.0.2 to 6.1.0.
- [Release notes](https://github.com/testing-library/eslint-plugin-testing-library/releases)
- [Changelog](https://github.com/testing-library/eslint-plugin-testing-library/blob/main/.releaserc.json)
- [Commits](https://github.com/testing-library/eslint-plugin-testing-library/compare/v6.0.2...v6.1.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-testing-library
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-21 14:06:25 +02:00
dependabot[bot]
24b6cb6b00 chore(deps-dev): bump @types/react from 18.2.28 to 18.2.31
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 18.2.28 to 18.2.31.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-21 14:05:46 +02:00
dependabot[bot]
f75c412295 chore(deps): bump pnpm/action-setup from 2.2.4 to 2.4.0
Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 2.2.4 to 2.4.0.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v2.2.4...v2.4.0)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-21 14:05:14 +02:00
dependabot[bot]
8bf51997ac chore(deps): bump docker/setup-buildx-action from 2 to 3
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-21 14:04:33 +02:00
dependabot[bot]
55084119f6 chore(deps-dev): bump @typescript-eslint/eslint-plugin
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.7.5 to 6.8.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.8.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-21 14:01:29 +02:00
dependabot[bot]
e816b77b25 chore(deps): bump docker/build-push-action from 4 to 5
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4 to 5.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-21 14:00:09 +02:00
dependabot[bot]
86daa95258 chore(deps): bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-21 13:59:31 +02:00
dependabot[bot]
96499d2ef7 chore(deps): bump docker/setup-qemu-action from 2 to 3
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-21 13:58:52 +02:00
Nicolas Meienberger
7ded3aace4 fix(theme): detect system theme when theme cookie is not present 2023-10-21 13:58:08 +02:00
dependabot[bot]
30c7d6ea4d chore(deps-dev): bump tough-cookie from 4.1.2 to 4.1.3
Bumps [tough-cookie](https://github.com/salesforce/tough-cookie) from 4.1.2 to 4.1.3.
- [Release notes](https://github.com/salesforce/tough-cookie/releases)
- [Changelog](https://github.com/salesforce/tough-cookie/blob/master/CHANGELOG.md)
- [Commits](https://github.com/salesforce/tough-cookie/compare/v4.1.2...v4.1.3)

---
updated-dependencies:
- dependency-name: tough-cookie
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-18 10:00:06 +02:00
dependabot[bot]
00382f99d3 chore(deps): bump @babel/traverse from 7.23.0 to 7.23.2
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.23.0 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-18 09:00:58 +02:00
dependabot[bot]
8682442735 chore(deps-dev): bump word-wrap from 1.2.3 to 1.2.5
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.5.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.5)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-18 08:58:58 +02:00
Nicolas Meienberger
2847ceab78 New translations en.json (Swedish) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
ec228bb071 New translations en.json (English) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
81fe58c54d New translations en.json (French) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
5711910aa0 New translations en.json (Chinese Traditional) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
1f3cafcc4c New translations en.json (Chinese Simplified) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
dc7b5a4c78 New translations en.json (Japanese) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
e2641d64da New translations en.json (Turkish) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
8a7d2cfd56 New translations en.json (Greek) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
7197ae712a New translations en.json (Portuguese, Brazilian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
23f2f9fe53 New translations en.json (Vietnamese) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
4985e57197 New translations en.json (English) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
ae60ce162d New translations en.json (Chinese Traditional) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
7fe2675c72 New translations en.json (Chinese Simplified) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
c6265b48eb New translations en.json (Ukrainian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
733ac5bbed New translations en.json (Swedish) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
20a0461ee6 New translations en.json (Serbian (Cyrillic)) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
deeaaf012c New translations en.json (Russian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
adbb347f28 New translations en.json (Portuguese) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
60222dddd4 New translations en.json (Polish) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
2f34560fa2 New translations en.json (Norwegian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
26f2ba4e46 New translations en.json (Dutch) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
01fe1445af New translations en.json (Korean) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
e68369f532 New translations en.json (Japanese) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
bedb07bc8f New translations en.json (Italian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
238c43440b New translations en.json (Hungarian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
7c4edde4e7 New translations en.json (Finnish) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
7f130b2978 New translations en.json (Greek) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
4850e348fe New translations en.json (German) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
5468bb08d5 New translations en.json (Danish) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
b11ba9b532 New translations en.json (Czech) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
b8c5d50d45 New translations en.json (Catalan) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
f6fcddce9d New translations en.json (Arabic) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
7f9a5dded6 New translations en.json (Afrikaans) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
6affffa83e New translations en.json (Spanish) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
82f7705650 New translations en.json (French) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
985c8502ce New translations en.json (Romanian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
ab68c055af New translations en.json (Turkish) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
e41f68cced New translations en.json (Hebrew) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
96305bf13b New translations en.json (Hungarian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
9dd46d7397 New translations en.json (Turkish) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
03efbda77d New translations en.json (Russian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
13cd46450f New translations en.json (Greek) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
4cf0ae2592 New translations en.json (Russian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
b81fd4db36 New translations en.json (Portuguese, Brazilian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
7176971224 New translations en.json (Vietnamese) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
38bca76219 New translations en.json (English) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
98c723100d New translations en.json (Chinese Traditional) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
043bb7f894 New translations en.json (Chinese Simplified) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
8920fdb33d New translations en.json (Ukrainian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
ce5d9520f4 New translations en.json (Swedish) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
f43988a561 New translations en.json (Serbian (Cyrillic)) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
a1a1cab530 New translations en.json (Russian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
c9e57eb01d New translations en.json (Portuguese) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
0719e4782d New translations en.json (Polish) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
870a737c65 New translations en.json (Norwegian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
f0cd9bf13f New translations en.json (Dutch) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
0c175fee74 New translations en.json (Korean) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
752c1db155 New translations en.json (Japanese) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
725ba21144 New translations en.json (Italian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
341c433385 New translations en.json (Hungarian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
8758393620 New translations en.json (Finnish) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
53a9a9db3d New translations en.json (Greek) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
2c1f5eacb8 New translations en.json (German) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
1edea9bea6 New translations en.json (Danish) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
134d9212fa New translations en.json (Czech) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
11700a3f10 New translations en.json (Catalan) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
db506509e9 New translations en.json (Arabic) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
481996ec76 New translations en.json (Afrikaans) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
3fcba91e5b New translations en.json (Spanish) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
4d86d80e3e New translations en.json (French) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
ac12afb1f2 New translations en.json (Romanian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
886edeb8b0 New translations en.json (Turkish) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
9779555526 New translations en.json (Hebrew) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
884f1d3640 New translations en.json (Greek) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
faeab592c1 New translations en.json (Russian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
143a3ebd0a New translations en.json (Turkish) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
f40501b3a0 New translations en.json (Portuguese, Brazilian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
56cb75831d New translations en.json (Vietnamese) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
32dc6c28ba New translations en.json (English) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
c25d1dec69 New translations en.json (Chinese Traditional) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
adabdac2a9 New translations en.json (Chinese Simplified) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
b61cc75a92 New translations en.json (Ukrainian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
86aa9a9cc4 New translations en.json (Swedish) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
bbdc40f93e New translations en.json (Serbian (Cyrillic)) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
6cfe41a64f New translations en.json (Russian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
cfa308bb04 New translations en.json (Portuguese) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
f197551902 New translations en.json (Polish) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
f8595d205a New translations en.json (Norwegian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
ce98159b2d New translations en.json (Dutch) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
5bb6ea047e New translations en.json (Korean) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
fbab23305e New translations en.json (Japanese) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
9a08b8de67 New translations en.json (Italian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
c982fc639b New translations en.json (Hungarian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
66e6d93ef5 New translations en.json (Finnish) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
4e63224b07 New translations en.json (Greek) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
c8857d0ecb New translations en.json (German) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
a9f5349aa8 New translations en.json (Danish) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
d959a1b7d8 New translations en.json (Czech) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
ec9112e941 New translations en.json (Catalan) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
4577adb008 New translations en.json (Arabic) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
fae4c7f9bc New translations en.json (Afrikaans) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
12ad9045c9 New translations en.json (Spanish) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
32768252fa New translations en.json (French) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
bb035cb84c New translations en.json (Romanian) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
cf5f14c0bb New translations en.json (Turkish) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
b0794eae47 New translations en.json (Hebrew) 2023-10-18 08:49:45 +02:00
Nicolas Meienberger
a90fbd869c chore: bump next to 13.5.5 2023-10-18 08:44:34 +02:00
dependabot[bot]
7ff9930893 chore(deps): bump actions/download-artifact from 2 to 3
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 2 to 3.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-18 08:40:16 +02:00
dependabot[bot]
b57bc8595a chore(deps): bump pnpm/action-setup from 2.2.4 to 2.4.0
Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 2.2.4 to 2.4.0.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v2.2.4...v2.4.0)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-18 08:39:07 +02:00
Nicolas Meienberger
ab950340e2 chore: bump @tabler/core to 1.0.0-beta20 2023-10-18 08:36:02 +02:00
dependabot[bot]
f928b5a056 chore(deps): bump winston from 3.9.0 to 3.11.0
Bumps [winston](https://github.com/winstonjs/winston) from 3.9.0 to 3.11.0.
- [Release notes](https://github.com/winstonjs/winston/releases)
- [Changelog](https://github.com/winstonjs/winston/blob/master/CHANGELOG.md)
- [Commits](https://github.com/winstonjs/winston/compare/v3.9.0...v3.11.0)

---
updated-dependencies:
- dependency-name: winston
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-18 08:02:14 +02:00
Nicolas Meienberger
43415f9e14 chore: bump next-safe-action to 4.0.1 2023-10-18 08:00:13 +02:00
dependabot[bot]
57dc28f518 chore(deps): bump zod from 3.21.4 to 3.22.4
Bumps [zod](https://github.com/colinhacks/zod) from 3.21.4 to 3.22.4.
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Changelog](https://github.com/colinhacks/zod/blob/master/CHANGELOG.md)
- [Commits](https://github.com/colinhacks/zod/compare/v3.21.4...v3.22.4)

---
updated-dependencies:
- dependency-name: zod
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-18 07:38:47 +02:00
dependabot[bot]
ee17e0cb78 chore(deps): bump actions/upload-pages-artifact from 1 to 2
Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 1 to 2.
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/v1...v2)

---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-18 07:38:07 +02:00
dependabot[bot]
468fd0e07a chore(deps): bump actions/upload-artifact from 2 to 3
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2 to 3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-18 07:37:00 +02:00
dependabot[bot]
4dc0885004 chore(deps): bump actions/github-script from 4 to 6
Bumps [actions/github-script](https://github.com/actions/github-script) from 4 to 6.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-18 07:36:25 +02:00
dependabot[bot]
1dfdbec246 chore(deps): bump @hookform/resolvers from 3.3.1 to 3.3.2
Bumps [@hookform/resolvers](https://github.com/react-hook-form/resolvers) from 3.3.1 to 3.3.2.
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v3.3.1...v3.3.2)

---
updated-dependencies:
- dependency-name: "@hookform/resolvers"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-18 07:35:51 +02:00
dependabot[bot]
fd7a5aa0d0 chore(deps): bump @tabler/icons-react from 2.38.0 to 2.39.0
Bumps [@tabler/icons-react](https://github.com/tabler/tabler-icons/tree/HEAD/packages/icons-react) from 2.38.0 to 2.39.0.
- [Release notes](https://github.com/tabler/tabler-icons/releases)
- [Commits](https://github.com/tabler/tabler-icons/commits/v2.39.0/packages/icons-react)

---
updated-dependencies:
- dependency-name: "@tabler/icons-react"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-18 07:34:55 +02:00
Nicolas Meienberger
27f5ccda82
Merge pull request #763 from runtipi/release/2.0.6
Release/2.0.6
2023-10-17 08:45:29 +02:00
208 changed files with 4593 additions and 4671 deletions

View file

@ -364,11 +364,47 @@
"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": "meienberger",
"projectOwner": "runtipi",
"repoType": "github",
"repoHost": "https://github.com",
"skipCi": true,

View file

@ -11,65 +11,95 @@ 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@v3
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
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@v3
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push images
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
platforms: linux/amd64
push: true
tags: meienberger/runtipi:${{ needs.create-tag.outputs.tagname }}
cache-from: type=registry,ref=meienberger/runtipi:buildcache
cache-to: type=registry,ref=meienberger/runtipi:buildcache,mode=max
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@v3
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
- uses: pnpm/action-setup@v2.2.4
- uses: pnpm/action-setup@v2.4.0
name: Install pnpm
id: pnpm-install
with:
@ -99,51 +129,37 @@ jobs:
run: pnpm -r --filter cli package
- name: Upload CLI
uses: actions/upload-artifact@v2
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]
needs: [create-tag, build-images, build-cli, build-worker]
steps:
- name: Download CLI
uses: actions/download-artifact@v2
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: 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

View file

@ -8,67 +8,98 @@ 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@v3
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v3
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
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push images
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: meienberger/runtipi:${{ needs.get-tag.outputs.tag }}
cache-from: type=registry,ref=meienberger/runtipi:buildcache
cache-to: type=registry,ref=meienberger/runtipi:buildcache,mode=max
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@v3
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
- uses: pnpm/action-setup@v2.2.4
- uses: pnpm/action-setup@v2.4.0
name: Install pnpm
id: pnpm-install
with:
@ -92,80 +123,49 @@ 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
- name: Upload CLI
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
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@v3
- 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:
- name: Download CLI
uses: actions/download-artifact@v2
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: 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]

View file

@ -1,6 +1,6 @@
name: Tipi CI
on:
push:
pull_request:
env:
ROOT_FOLDER: /runtipi
@ -38,14 +38,14 @@ jobs:
--health-retries 5
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
- uses: pnpm/action-setup@v2.2.4
- uses: pnpm/action-setup@v2.4.0
name: Install pnpm
id: pnpm-install
with:
@ -76,7 +76,7 @@ jobs:
- name: Get number of CPU cores
id: cpu-cores
uses: SimenB/github-actions-cpu-cores@v1
uses: SimenB/github-actions-cpu-cores@v2
- name: Run tests
run: pnpm run test --max-workers ${{ steps.cpu-cores.outputs.count }}
@ -101,14 +101,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
- uses: pnpm/action-setup@v2.2.4
- uses: pnpm/action-setup@v2.4.0
name: Install pnpm
id: pnpm-install
with:

View file

@ -15,6 +15,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: 'Dependency Review'
uses: actions/dependency-review-action@v3

View file

@ -29,7 +29,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install SSH key
uses: shimataro/ssh-key-action@v2
@ -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:
@ -110,9 +110,9 @@ jobs:
runs-on: ubuntu-latest
needs: [deploy]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2.2.4
- uses: pnpm/action-setup@v2.4.0
name: Install pnpm
id: pnpm-install
with:
@ -132,9 +132,9 @@ jobs:
restore-keys: |
${{ runner.os }}-pnpm-store-
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
- name: Create .env.e2e file with Droplet IP
run: |
@ -181,7 +181,7 @@ jobs:
uses: actions/configure-pages@v3
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
uses: actions/upload-pages-artifact@v2
with:
path: playwright-report/

View file

@ -1,41 +0,0 @@
name: Release candidate
on:
workflow_dispatch:
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 "tag=${TAG}" >> $GITHUB_OUTPUT
- name: Build and push images
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64
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

View file

@ -3,69 +3,101 @@ 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@v3
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v3
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
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push images
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: meienberger/runtipi:latest,meienberger/runtipi:${{ needs.get-tag.outputs.tag }}
cache-from: type=registry,ref=meienberger/runtipi:buildcache
cache-to: type=registry,ref=meienberger/runtipi:buildcache,mode=max
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@v3
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
- uses: pnpm/action-setup@v2.2.4
- uses: pnpm/action-setup@v2.4.0
name: Install pnpm
id: pnpm-install
with:
@ -89,78 +121,49 @@ 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
- name: Upload CLI
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
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@v3
- 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:
- name: Download CLI
uses: actions/download-artifact@v2
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
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]
@ -175,12 +178,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Promote release
uses: actions/github-script@v4
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const id = '${{ needs.publish-release.outputs.id }}';
github.repos.updateRelease({
github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: id,

4
.gitignore vendored
View file

@ -54,8 +54,7 @@ node_modules/
/data/
/repos/
/apps/
traefik/shared
traefik/tls
/traefik/
# media folder
media
@ -67,3 +66,4 @@ media
temp
./traefik/
/user-config/

View file

@ -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
@ -33,7 +33,8 @@ RUN npm run build
FROM node_base AS app
ENV NODE_ENV production
# USER node
USER node
WORKDIR /app

View file

@ -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}

View file

@ -1,9 +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-38-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)
@ -14,6 +12,8 @@
![Build](https://github.com/runtipi/runtipi/workflows/Tipi%20CI/badge.svg)
[![Crowdin](https://badges.crowdin.net/runtipi/localized.svg)](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!
#### Join the discussion
[![Discord](https://img.shields.io/discord/976934649643294750?label=discord&logo=discord)](https://discord.gg/Bu9qEPnHsc)
@ -73,36 +73,36 @@ 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/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" 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/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" 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/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> <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/meienberger/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/meienberger/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/meienberger/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/meienberger/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/meienberger/runtipi/commits?author=blushell" title="Code">💻</a></td>
<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>
</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/meienberger/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/meienberger/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/meienberger/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/meienberger/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/meienberger/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/meienberger/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/meienberger/runtipi/commits?author=iBicha" title="Code">💻</a></td>
<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>
</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/meienberger/runtipi/commits?author=fsackur" title="Code">💻</a> <a href="https://github.com/meienberger/runtipi/commits?author=fsackur" title="Documentation">📖</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/meienberger/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/meienberger/runtipi/commits?author=ruibaby" title="Code">💻</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/meienberger/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/meienberger/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/meienberger/runtipi/commits?author=mcmeel" title="Code">💻</a> <a href="https://github.com/meienberger/runtipi/commits?author=mcmeel" title="Documentation">📖</a></td>
<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>
@ -111,16 +111,20 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<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/meienberger/runtipi/commits?author=amusingimpala75" title="Code">💻</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"/><br /><sub><b>Stavros</b></sub></a><br /><a href="#translation-steveiliop56" title="Translation">🌍</a> <a href="https://github.com/meienberger/runtipi/commits?author=steveiliop56" title="Code">💻</a> <a href="https://github.com/meienberger/runtipi/commits?author=steveiliop56" title="Tests">⚠️</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/meienberger/runtipi/commits?author=JigSawFr" title="Code">💻</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/meienberger/runtipi/commits?author=DireMunchkin" title="Code">💻</a></td>
<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/meienberger/runtipi/commits?author=itsrllyhim" title="Code">💻</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>
</tr>
</tbody>
</table>

View file

@ -55,6 +55,43 @@ services:
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:
build:
context: .
@ -65,6 +102,8 @@ services:
condition: service_healthy
tipi-redis:
condition: service_healthy
tipi-worker:
condition: service_healthy
env_file:
- .env
environment:
@ -84,7 +123,7 @@ services:
- ${PWD}/apps:/runtipi/apps
- ${PWD}/logs:/app/logs
- ${PWD}/traefik:/runtipi/traefik
- ${STORAGE_PATH}:/app/storage
- ${STORAGE_PATH:-$PWD}:/app/storage
labels:
traefik.enable: true
traefik.http.services.dashboard.loadbalancer.server.port: 3000

View file

@ -55,6 +55,40 @@ services:
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: .
@ -65,6 +99,8 @@ services:
condition: service_healthy
tipi-redis:
condition: service_healthy
tipi-worker:
condition: service_healthy
env_file:
- .env
environment:

View file

@ -2,8 +2,10 @@ 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);
@ -31,3 +33,49 @@ test('user can change their password', async ({ page }) => {
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();
});

View file

@ -0,0 +1,58 @@
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();
});

View file

@ -1,10 +1,12 @@
import { clearDatabase } from './db';
import { setSettings } from './settings';
/**
*
*/
async function globalSetup() {
await clearDatabase();
await setSettings({});
}
export default globalSetup;

8
e2e/helpers/settings.ts Normal file
View file

@ -0,0 +1,8 @@
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));
};

1
next-env.d.ts vendored
View file

@ -1,6 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View file

@ -6,7 +6,6 @@ const nextConfig = {
transpilePackages: ['@runtipi/shared'],
experimental: {
serverComponentsExternalPackages: ['bullmq'],
serverActions: true,
},
serverRuntimeConfig: {
INTERNAL_IP: process.env.INTERNAL_IP,

View file

@ -1,6 +1,6 @@
{
"name": "runtipi",
"version": "2.0.6",
"version": "2.2.1",
"description": "A homeserver for everyone",
"scripts": {
"knip": "knip",
@ -11,7 +11,7 @@
"test:client": "jest --colors --selectProjects client --",
"test:server": "jest --colors --selectProjects server --",
"test:vite": "dotenv -e .env.test -- vitest run --coverage",
"dev": "npm run db:migrate && next dev",
"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",
@ -32,45 +32,48 @@
"tsc": "tsc"
},
"dependencies": {
"@hookform/resolvers": "^3.3.1",
"@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-beta19",
"@tabler/icons-react": "^2.38.0",
"argon2": "^0.31.1",
"bullmq": "^4.12.3",
"@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": "13.5.4",
"next-client-cookies": "^1.0.5",
"next-intl": "^2.20.2",
"next-safe-action": "^3.4.0",
"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.47.0",
"react-hook-form": "^7.48.2",
"react-hot-toast": "^2.4.1",
"react-markdown": "^9.0.0",
"react-select": "^5.7.7",
"react-tooltip": "^5.21.5",
"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.2",
"sass": "^1.69.5",
"semver": "^7.5.4",
"sharp": "0.32.6",
"swr": "^2.2.4",
@ -78,57 +81,57 @@
"uuid": "^9.0.1",
"validator": "^13.11.0",
"winston": "^3.11.0",
"zod": "^3.21.4",
"zustand": "^4.4.3"
"zod": "^3.22.4",
"zustand": "^4.4.6"
},
"devDependencies": {
"@babel/core": "^7.23.0",
"@faker-js/faker": "^8.1.0",
"@playwright/test": "^1.38.1",
"@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.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.2",
"@types/jest": "^29.5.5",
"@types/lodash.merge": "^4.6.7",
"@types/node": "20.8.4",
"@types/pg": "^8.10.5",
"@types/react": "18.2.28",
"@types/react-dom": "18.2.13",
"@types/semver": "^7.5.3",
"@types/uuid": "^9.0.5",
"@types/validator": "^13.11.2",
"@typescript-eslint/eslint-plugin": "^6.7.5",
"@typescript-eslint/parser": "^6.7.5",
"@vitejs/plugin-react": "^4.1.0",
"@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.51.0",
"eslint": "8.52.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.1.0",
"eslint-config-next": "13.5.4",
"eslint-config-next": "14.0.3",
"eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jest": "^27.4.2",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-jest": "^27.6.0",
"eslint-plugin-jest-dom": "^5.1.0",
"eslint-plugin-jsx-a11y": "^6.7.1",
"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.0.2",
"eslint-plugin-testing-library": "^6.1.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"knip": "^2.33.1",
"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.13.0",
"tsx": "^3.14.0",
"typescript": "5.2.2",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "^0.34.6",
@ -148,8 +151,6 @@
},
"homepage": "https://github.com/runtipi/runtipi#readme",
"pnpm": {
"patchedDependencies": {
"next-safe-action@3.4.0": "patches/next-safe-action@3.4.0.patch"
}
"patchedDependencies": {}
}
}

View file

@ -4,10 +4,12 @@ services:
tipi-reverse-proxy:
container_name: tipi-reverse-proxy
image: traefik:v2.8
restart: on-failure
restart: unless-stopped
depends_on:
- tipi-dashboard
ports:
- ${NGINX_PORT-80}:80
- ${NGINX_PORT_SSL-443}:443
- ${NGINX_PORT:-80}:80
- ${NGINX_PORT_SSL:-443}:443
command: --providers.docker
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
@ -19,10 +21,10 @@ services:
tipi-db:
container_name: tipi-db
image: postgres:14
restart: on-failure
restart: unless-stopped
stop_grace_period: 1m
ports:
- 5432:5432
- ${POSTGRES_PORT:-5432}:5432
volumes:
- ./data/postgres:/var/lib/postgresql/data
environment:
@ -40,7 +42,7 @@ services:
tipi-redis:
container_name: tipi-redis
image: redis:7.2.0
restart: on-failure
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD}
ports:
- 6379:6379
@ -54,12 +56,16 @@ services:
networks:
- tipi_main_network
tipi-dashboard:
image: meienberger/runtipi:${TIPI_VERSION}
restart: on-failure
container_name: tipi-dashboard
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
@ -70,13 +76,46 @@ services:
environment:
NODE_ENV: production
volumes:
- ./.env:/runtipi/.env
# 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
- ./traefik:/runtipi/traefik
- ${STORAGE_PATH}:/app/storage
- ${STORAGE_PATH:-.}:/app/storage
env_file:
- .env
environment:
NODE_ENV: production
labels:
# Main
traefik.enable: true

View file

@ -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'),

View file

@ -1,26 +1,27 @@
{
"name": "@runtipi/cli",
"version": "2.0.6",
"version": "2.1.0",
"description": "",
"main": "index.js",
"bin": "dist/index.js",
"scripts": {
"test": "dotenv -e .env.test vitest -- --coverage --watch=false",
"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"
"tsc": "tsc --noEmit",
"knip": "knip"
},
"pkg": {
"assets": "assets/**/*",
"targets": [
"node18-linux-x64",
"node18-linux-arm64"
"node20-linux-x64",
"node20-linux-arm64"
],
"outputPath": "dist/bin"
},
@ -28,37 +29,32 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@faker-js/faker": "^8.1.0",
"@types/cli-progress": "^3.11.3",
"@types/node": "20.8.4",
"@types/web-push": "^3.6.1",
"@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.4.11",
"vite": "^4.5.0",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "^0.34.6"
},
"dependencies": {
"@runtipi/postgres-migrations": "^5.3.0",
"@runtipi/shared": "workspace:^",
"axios": "^1.5.1",
"axios": "^1.6.0",
"boxen": "^7.1.1",
"bullmq": "^4.12.3",
"bullmq": "^4.13.0",
"chalk": "^5.3.0",
"cli-progress": "^3.12.0",
"cli-spinners": "^2.9.1",
"commander": "^11.0.0",
"commander": "^11.1.0",
"dotenv": "^16.3.1",
"ioredis": "^5.3.2",
"log-update": "^5.0.1",
"pg": "^8.11.3",
"semver": "^7.5.4",
"systeminformation": "^5.21.11",
"web-push": "^3.6.6",
"zod": "^3.21.4"
"zod": "^3.22.4"
}
}

View file

@ -1,294 +1,73 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable no-restricted-syntax */
import fs from 'fs';
import path from 'path';
import pg from 'pg';
import { Queue, QueueEvents } from 'bullmq';
import { SystemEvent, eventSchema } from '@runtipi/shared';
import { getEnv } from '@/utils/environment/environment';
import { pathExists } from '@/utils/fs-helpers';
import { compose } from '@/utils/docker-helpers';
import { copyDataDir, generateEnvFile } from './app.helpers';
import { fileLogger } from '@/utils/logger/file-logger';
import { logger } from '@/utils/logger/logger';
import { TerminalSpinner } from '@/utils/logger/terminal-spinner';
import { execAsync } from '@/utils/exec-async/execAsync';
const getDbClient = async () => {
const { postgresDatabase, postgresUsername, postgresPassword, postgresPort } = getEnv();
const client = new pg.Client({
host: '127.0.0.1',
database: postgresDatabase,
user: postgresUsername,
password: postgresPassword,
port: Number(postgresPort),
});
await client.connect();
return client;
};
export class AppExecutors {
private readonly logger;
constructor() {
this.logger = fileLogger;
this.logger = logger;
}
private handleAppError = (err: unknown) => {
if (err instanceof Error) {
this.logger.error(`An error occurred: ${err.message}`);
return { success: false, message: err.message };
}
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 { success: false, message: `An error occurred: ${err}` };
return { queue, queueEvents };
};
private getAppPaths = (appId: string) => {
const { rootFolderHost, storagePath, appsRepoId } = getEnv();
const appDataDirPath = path.join(storagePath, 'app-data', appId);
const appDirPath = path.join(rootFolderHost, 'apps', appId);
const configJsonPath = path.join(appDirPath, 'config.json');
const repoPath = path.join(rootFolderHost, 'repos', appsRepoId, 'apps', appId);
return { appDataDirPath, appDirPath, configJsonPath, repoPath };
};
/**
* Given an app id, ensures that the app folder exists in the apps folder
* If not, copies the app folder from the repo
* @param {string} appId - App id
*/
private ensureAppDir = async (appId: string) => {
const { rootFolderHost } = getEnv();
const { appDirPath, repoPath } = this.getAppPaths(appId);
const dockerFilePath = path.join(rootFolderHost, 'apps', appId, 'docker-compose.yml');
if (!(await pathExists(dockerFilePath))) {
// delete eventual app folder if exists
this.logger.info(`Deleting app ${appId} folder if exists`);
await fs.promises.rm(appDirPath, { recursive: true, force: true });
// Copy app folder from repo
this.logger.info(`Copying app ${appId} from repo ${getEnv().appsRepoId}`);
await fs.promises.cp(repoPath, appDirPath, { recursive: true });
}
};
/**
* Install an app from the repo
* @param {string} appId - The id of the app to install
* @param {Record<string, unknown>} config - The config of the app
*/
public installApp = async (appId: string, config: Record<string, unknown>) => {
try {
if (process.getuid && process.getgid) {
this.logger.info(`Installing app ${appId} as User ID: ${process.getuid()}, Group ID: ${process.getgid()}`);
} else {
this.logger.info(`Installing app ${appId}. No User ID or Group ID found.`);
}
const { rootFolderHost, appsRepoId } = getEnv();
const { appDirPath, repoPath, appDataDirPath } = this.getAppPaths(appId);
// Check if app exists in repo
const apps = await fs.promises.readdir(path.join(rootFolderHost, 'repos', appsRepoId, 'apps'));
if (!apps.includes(appId)) {
this.logger.error(`App ${appId} not found in repo ${appsRepoId}`);
return { success: false, message: `App ${appId} not found in repo ${appsRepoId}` };
}
// Delete app folder if exists
this.logger.info(`Deleting folder ${appDirPath} if exists`);
await fs.promises.rm(appDirPath, { recursive: true, force: true });
// Create app folder
this.logger.info(`Creating folder ${appDirPath}`);
await fs.promises.mkdir(appDirPath, { recursive: true });
// Copy app folder from repo
this.logger.info(`Copying folder ${repoPath} to ${appDirPath}`);
await fs.promises.cp(repoPath, appDirPath, { recursive: true });
// Create folder app-data folder
this.logger.info(`Creating folder ${appDataDirPath}`);
await fs.promises.mkdir(appDataDirPath, { recursive: true });
// Create app.env file
this.logger.info(`Creating app.env file for app ${appId}`);
await generateEnvFile(appId, config);
// Copy data dir
this.logger.info(`Copying data dir for app ${appId}`);
if (!(await pathExists(`${appDataDirPath}/data`))) {
await copyDataDir(appId);
}
await execAsync(`chmod -R a+rwx ${path.join(appDataDirPath)}`).catch(() => {
this.logger.error(`Error setting permissions for app ${appId}`);
});
// run docker-compose up
this.logger.info(`Running docker-compose up for app ${appId}`);
await compose(appId, 'up -d');
this.logger.info(`Docker-compose up for app ${appId} finished`);
return { success: true, message: `App ${appId} installed successfully` };
} catch (err) {
return this.handleAppError(err);
}
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
* @param {Record<string, unknown>} config - The config of the app
*/
public stopApp = async (appId: string, config: Record<string, unknown>, skipEnvGeneration = false) => {
try {
this.logger.info(`Stopping app ${appId}`);
public stopApp = async (appId: string) => {
const spinner = new TerminalSpinner(`Stopping app ${appId}`);
spinner.start();
this.logger.info(`Regenerating app.env file for app ${appId}`);
await this.ensureAppDir(appId);
const jobid = this.generateJobId({ appId, action: 'stop' });
if (!skipEnvGeneration) {
await generateEnvFile(appId, config);
}
await compose(appId, 'rm --force --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);
this.logger.info(`App ${appId} stopped`);
return { success: true, message: `App ${appId} stopped successfully` };
} catch (err) {
return this.handleAppError(err);
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, config: Record<string, unknown>) => {
try {
const { appDataDirPath } = this.getAppPaths(appId);
public startApp = async (appId: string) => {
const spinner = new TerminalSpinner(`Starting app ${appId}`);
spinner.start();
this.logger.info(`Starting app ${appId}`);
const jobid = this.generateJobId({ appId, action: 'start' });
this.logger.info(`Regenerating app.env file for app ${appId}`);
await this.ensureAppDir(appId);
await generateEnvFile(appId, config);
await compose(appId, 'up --detach --force-recreate --remove-orphans --pull always');
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);
this.logger.info(`App ${appId} started`);
await queueEvents.close();
await queue.close();
this.logger.info(`Setting permissions for app ${appId}`);
await execAsync(`chmod -R a+rwx ${path.join(appDataDirPath)}`).catch(() => {
this.logger.error(`Error setting permissions for app ${appId}`);
});
return { success: true, message: `App ${appId} started successfully` };
} catch (err) {
return this.handleAppError(err);
}
};
public uninstallApp = async (appId: string, config: Record<string, unknown>) => {
try {
const { appDirPath, appDataDirPath } = this.getAppPaths(appId);
this.logger.info(`Uninstalling app ${appId}`);
this.logger.info(`Regenerating app.env file for app ${appId}`);
await this.ensureAppDir(appId);
await generateEnvFile(appId, config);
await compose(appId, 'down --remove-orphans --volumes --rmi all');
this.logger.info(`Deleting folder ${appDirPath}`);
await fs.promises.rm(appDirPath, { recursive: true, force: true }).catch((err) => {
this.logger.error(`Error deleting folder ${appDirPath}: ${err.message}`);
});
this.logger.info(`Deleting folder ${appDataDirPath}`);
await fs.promises.rm(appDataDirPath, { recursive: true, force: true }).catch((err) => {
this.logger.error(`Error deleting folder ${appDataDirPath}: ${err.message}`);
});
this.logger.info(`App ${appId} uninstalled`);
return { success: true, message: `App ${appId} uninstalled successfully` };
} catch (err) {
return this.handleAppError(err);
}
};
public updateApp = async (appId: string, config: Record<string, unknown>) => {
try {
const { appDirPath, repoPath } = this.getAppPaths(appId);
this.logger.info(`Updating app ${appId}`);
await this.ensureAppDir(appId);
await generateEnvFile(appId, config);
await compose(appId, 'up --detach --force-recreate --remove-orphans');
await compose(appId, 'down --rmi all --remove-orphans');
this.logger.info(`Deleting folder ${appDirPath}`);
await fs.promises.rm(appDirPath, { recursive: true, force: true });
this.logger.info(`Copying folder ${repoPath} to ${appDirPath}`);
await fs.promises.cp(repoPath, appDirPath, { recursive: true });
await compose(appId, 'pull');
return { success: true, message: `App ${appId} updated successfully` };
} catch (err) {
return this.handleAppError(err);
}
};
public regenerateAppEnv = async (appId: string, config: Record<string, unknown>) => {
try {
this.logger.info(`Regenerating app.env file for app ${appId}`);
await this.ensureAppDir(appId);
await generateEnvFile(appId, config);
return { success: true, message: `App ${appId} env file regenerated successfully` };
} catch (err) {
return this.handleAppError(err);
}
};
/**
* Start all apps with status running
*/
public startAllApps = async () => {
const spinner = new TerminalSpinner('Starting apps...');
const client = await getDbClient();
try {
// Get all apps with status running
const { rows } = await client.query(`SELECT * FROM app WHERE status = 'running'`);
// Update all apps with status different than running or stopped to stopped
await client.query(`UPDATE app SET status = 'stopped' WHERE status != 'stopped' AND status != 'running' AND status != 'missing'`);
// Start all apps
for (const row of rows) {
spinner.setMessage(`Starting app ${row.id}`);
spinner.start();
const { id, config } = row;
const { success } = await this.startApp(id, config);
if (!success) {
this.logger.error(`Error starting app ${id}`);
await client.query(`UPDATE app SET status = 'stopped' WHERE id = '${id}'`);
spinner.fail(`Error starting app ${id}`);
} else {
await client.query(`UPDATE app SET status = 'running' WHERE id = '${id}'`);
spinner.done(`App ${id} started`);
}
}
} catch (err) {
this.logger.error(`Error starting apps: ${err}`);
spinner.fail(`Error starting apps see logs for details (logs/error.log)`);
} finally {
await client.end();
if (!result.success) {
spinner.fail(`Failed to start app ${appId} see logs for more details (logs/error.log)`);
} else {
spinner.done(`App ${appId} started`);
}
};
}

View file

@ -1,3 +1,2 @@
export { AppExecutors } from './app/app.executors';
export { RepoExecutors } from './repo/repo.executors';
export { SystemExecutors } from './system/system.executors';

View file

@ -1,12 +0,0 @@
import crypto from 'crypto';
/**
* Given a repo url, return a hash of it to be used as a folder name
*
* @param {string} repoUrl
*/
export const getRepoHash = (repoUrl: string) => {
const hash = crypto.createHash('sha256');
hash.update(repoUrl);
return hash.digest('hex');
};

View file

@ -1,7 +1,5 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable no-await-in-loop */
import { Queue } from 'bullmq';
import { Redis } from 'ioredis';
import fs from 'fs';
import cliProgress from 'cli-progress';
import semver from 'semver';
@ -9,20 +7,14 @@ import axios from 'axios';
import boxen from 'boxen';
import path from 'path';
import { spawn } from 'child_process';
import si from 'systeminformation';
import { Stream } from 'stream';
import dotenv from 'dotenv';
import { SystemEvent } from '@runtipi/shared';
import chalk from 'chalk';
import { killOtherWorkers } from 'src/services/watcher/watcher';
import { pathExists } from '@runtipi/shared';
import { AppExecutors } from '../app/app.executors';
import { copySystemFiles, generateSystemEnvFile, generateTlsCertificates } from './system.helpers';
import { copySystemFiles, generateSystemEnvFile } from './system.helpers';
import { TerminalSpinner } from '@/utils/logger/terminal-spinner';
import { pathExists } from '@/utils/fs-helpers';
import { getEnv } from '@/utils/environment/environment';
import { fileLogger } from '@/utils/logger/file-logger';
import { runPostgresMigrations } from '@/utils/migrations/run-migration';
import { getUserIds } from '@/utils/environment/user';
import { logger } from '@/utils/logger/logger';
import { execAsync } from '@/utils/exec-async/execAsync';
export class SystemExecutors {
@ -34,7 +26,7 @@ export class SystemExecutors {
constructor() {
this.rootFolder = process.cwd();
this.logger = fileLogger;
this.logger = logger;
this.envFile = path.join(this.rootFolder, '.env');
}
@ -49,66 +41,9 @@ export class SystemExecutors {
return { success: false, message: `An error occurred: ${err}` };
};
private getSystemLoad = async () => {
const { currentLoad } = await si.currentLoad();
const mem = await si.mem();
const [disk0] = await si.fsSize();
return {
cpu: { load: currentLoad },
memory: { total: mem.total, used: mem.used, available: mem.available },
disk: { total: disk0?.size, used: disk0?.used, available: disk0?.available },
};
};
private ensureFilePermissions = async (rootFolderHost: string) => {
const logger = new TerminalSpinner('');
const filesAndFolders = [
path.join(rootFolderHost, 'apps'),
path.join(rootFolderHost, 'logs'),
path.join(rootFolderHost, 'repos'),
path.join(rootFolderHost, 'state'),
path.join(rootFolderHost, 'traefik'),
path.join(rootFolderHost, '.env'),
path.join(rootFolderHost, 'VERSION'),
path.join(rootFolderHost, 'docker-compose.yml'),
];
const files600 = [path.join(rootFolderHost, 'traefik', 'shared', 'acme.json')];
this.logger.info('Setting file permissions a+rwx on required files');
// Give permission to read and write to all files and folders for the current user
for (const fileOrFolder of filesAndFolders) {
if (await pathExists(fileOrFolder)) {
this.logger.info(`Setting permissions on ${fileOrFolder}`);
await execAsync(`chmod -R a+rwx ${fileOrFolder}`).catch(() => {
logger.fail(`Failed to set permissions on ${fileOrFolder}`);
});
this.logger.info(`Successfully set permissions on ${fileOrFolder}`);
}
}
this.logger.info('Setting file permissions 600 on required files');
for (const fileOrFolder of files600) {
if (await pathExists(fileOrFolder)) {
this.logger.info(`Setting permissions on ${fileOrFolder}`);
await execAsync(`chmod 600 ${fileOrFolder}`).catch(() => {
logger.fail(`Failed to set permissions on ${fileOrFolder}`);
});
this.logger.info(`Successfully set permissions on ${fileOrFolder}`);
}
}
};
public cleanLogs = async () => {
try {
const { rootFolderHost } = getEnv();
await fs.promises.rm(path.join(rootFolderHost, 'logs'), { recursive: true, force: true });
await fs.promises.mkdir(path.join(rootFolderHost, 'logs'));
await this.logger.flush();
this.logger.info('Logs cleaned successfully');
return { success: true, message: '' };
@ -117,20 +52,6 @@ export class SystemExecutors {
}
};
public systemInfo = async () => {
try {
const { rootFolderHost } = getEnv();
const systemLoad = await this.getSystemLoad();
await fs.promises.writeFile(path.join(rootFolderHost, 'state', 'system-info.json'), JSON.stringify(systemLoad, null, 2));
await fs.promises.chmod(path.join(rootFolderHost, 'state', 'system-info.json'), 0o777);
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.
@ -147,7 +68,7 @@ export class SystemExecutors {
for (const app of apps) {
spinner.setMessage(`Stopping ${app}...`);
spinner.start();
await appExecutor.stopApp(app, {}, true);
await appExecutor.stopApp(app);
spinner.done(`${app} stopped`);
}
}
@ -171,52 +92,30 @@ export class SystemExecutors {
* 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 (sudo = true, killWatchers = true) => {
public start = async () => {
const spinner = new TerminalSpinner('Starting Tipi...');
try {
const { isSudo } = getUserIds();
if (!sudo) {
console.log(
boxen(
"You are running in sudoless mode. While Tipi should work as expected, you'll probably run into permission issues and will have to manually fix them. We recommend running Tipi with sudo for beginners.",
{
title: '⛔Sudoless mode',
titleAlignment: 'center',
textAlignment: 'center',
padding: 1,
borderStyle: 'double',
borderColor: 'red',
margin: { top: 1, bottom: 1 },
width: 80,
},
),
);
}
this.logger.info('Killing other workers...');
if (killWatchers) {
await killOtherWorkers();
}
if (!isSudo && sudo) {
console.log(chalk.red('Tipi needs to run as root to start. Use sudo ./runtipi-cli start'));
throw new Error('Tipi needs to run as root to start. Use sudo ./runtipi-cli start');
}
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');
if (sudo) {
await this.ensureFilePermissions(this.rootFolder);
}
spinner.setMessage('Generating system env file...');
spinner.start();
this.logger.info('Generating system env file...');
@ -243,65 +142,6 @@ export class SystemExecutors {
await execAsync(`docker compose --env-file ${this.envFile} up --detach --remove-orphans --build`);
spinner.done('Containers started');
// start watcher cli in the background
spinner.setMessage('Starting watcher...');
spinner.start();
this.logger.info('Generating TLS certificates...');
await generateTlsCertificates({ domain: envMap.get('LOCAL_DOMAIN') });
if (killWatchers) {
this.logger.info('Starting watcher...');
const subprocess = spawn('./runtipi-cli', [process.argv[1] as string, 'watch'], { cwd: this.rootFolder, detached: true, stdio: ['ignore', 'ignore', 'ignore'] });
subprocess.unref();
}
spinner.done('Watcher started');
// Flush redis cache
this.logger.info('Flushing redis cache...');
const cache = new Redis({ host: '127.0.0.1', port: 6379, password: envMap.get('REDIS_PASSWORD'), lazyConnect: true });
await cache.connect();
await cache.flushdb();
await cache.quit();
this.logger.info('Starting queue...');
const queue = new Queue('events', { connection: { host: '127.0.0.1', port: 6379, password: envMap.get('REDIS_PASSWORD') } });
this.logger.info('Obliterating queue...');
await queue.obliterate({ force: true });
// Initial jobs
this.logger.info('Adding initial jobs to queue...');
await queue.add(`${Math.random().toString()}_system_info`, { type: 'system', command: 'system_info' } as SystemEvent);
await queue.add(`${Math.random().toString()}_repo_clone`, { type: 'repo', command: 'clone', url: envMap.get('APPS_REPO_URL') } as SystemEvent);
// Scheduled jobs
this.logger.info('Adding scheduled jobs to queue...');
await queue.add(`${Math.random().toString()}_repo_update`, { type: 'repo', command: 'update', url: envMap.get('APPS_REPO_URL') } as SystemEvent, { repeat: { pattern: '*/30 * * * *' } });
await queue.add(`${Math.random().toString()}_system_info`, { type: 'system', command: 'system_info' } as SystemEvent, { repeat: { pattern: '* * * * *' } });
this.logger.info('Closing queue...');
await queue.close();
spinner.setMessage('Running database migrations...');
spinner.start();
this.logger.info('Running database migrations...');
await runPostgresMigrations({
postgresHost: '127.0.0.1',
postgresDatabase: envMap.get('POSTGRES_DBNAME') as string,
postgresUsername: envMap.get('POSTGRES_USERNAME') as string,
postgresPassword: envMap.get('POSTGRES_PASSWORD') as string,
postgresPort: envMap.get('POSTGRES_PORT') as string,
});
spinner.done('Database migrations complete');
// Start all apps
const appExecutor = new AppExecutors();
this.logger.info('Starting all apps...');
await appExecutor.startAllApps();
console.log(
boxen(
`Visit: http://${envMap.get('INTERNAL_IP')}:${envMap.get(
@ -333,7 +173,7 @@ export class SystemExecutors {
public restart = async () => {
try {
await this.stop();
await this.start(true, false);
await this.start();
return { success: true, message: '' };
} catch (e) {
return this.handleSystemError(e);

View file

@ -2,12 +2,8 @@ import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { envMapToString, envStringToMap, settingsSchema } from '@runtipi/shared';
import chalk from 'chalk';
import { pathExists } from '@/utils/fs-helpers';
import { getRepoHash } from '../repo/repo.helpers';
import { fileLogger } from '@/utils/logger/file-logger';
import { execAsync } from '@/utils/exec-async/execAsync';
import { envMapToString, envStringToMap, pathExists, settingsSchema } from '@runtipi/shared';
import { logger } from '@/utils/logger/logger';
type EnvKeys =
| 'APPS_REPO_ID'
@ -32,14 +28,12 @@ type EnvKeys =
| 'REDIS_PASSWORD'
| 'LOCAL_DOMAIN'
| 'DEMO_MODE'
| 'GUEST_DASHBOARD'
| 'TIPI_GID'
| 'TIPI_UID'
// eslint-disable-next-line @typescript-eslint/ban-types
| (string & {});
const OLD_DEFAULT_REPO_URL = 'https://github.com/meienberger/runtipi-appstore';
const DEFAULT_REPO_URL = 'https://github.com/runtipi/runtipi-appstore';
/**
* Reads and returns the generated seed
*/
@ -146,172 +140,43 @@ export const generateSystemEnvFile = async () => {
const { data } = settings;
if (data.appsRepoUrl === OLD_DEFAULT_REPO_URL) {
data.appsRepoUrl = DEFAULT_REPO_URL;
}
const jwtSecret = envMap.get('JWT_SECRET') || (await deriveEntropy('jwt_secret'));
const repoId = getRepoHash(data.appsRepoUrl || DEFAULT_REPO_URL);
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('APPS_REPO_ID', repoId);
envMap.set('APPS_REPO_URL', data.appsRepoUrl || DEFAULT_REPO_URL);
envMap.set('TZ', Intl.DateTimeFormat().resolvedOptions().timeZone);
envMap.set('INTERNAL_IP', data.listenIp || getInternalIp());
envMap.set('DNS_IP', data.dnsIp || '9.9.9.9');
envMap.set('ARCHITECTURE', getArchitecture());
envMap.set('TIPI_VERSION', version);
envMap.set('JWT_SECRET', jwtSecret);
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('DOMAIN', data.domain || 'example.com');
envMap.set('STORAGE_PATH', data.storagePath || rootFolder);
envMap.set('POSTGRES_HOST', 'tipi-db');
envMap.set('POSTGRES_DBNAME', 'tipi');
envMap.set('POSTGRES_USERNAME', 'tipi');
envMap.set('POSTGRES_PASSWORD', postgresPassword);
envMap.set('POSTGRES_PORT', String(5432));
envMap.set('POSTGRES_PORT', String(data.postgresPort || 5432));
envMap.set('REDIS_HOST', 'tipi-redis');
envMap.set('REDIS_PASSWORD', redisPassword);
envMap.set('DEMO_MODE', String(data.demoMode || 'false'));
envMap.set('LOCAL_DOMAIN', data.localDomain || 'tipi.lan');
envMap.set('NODE_ENV', 'production');
const currentUserGroup = process.getgid ? String(process.getgid()) : '1000';
const currentUserId = process.getuid ? String(process.getuid()) : '1000';
envMap.set('TIPI_GID', currentUserGroup);
envMap.set('TIPI_UID', currentUserId);
envMap.set('DOMAIN', data.domain || 'example.com');
envMap.set('LOCAL_DOMAIN', data.localDomain || 'tipi.lan');
await fs.promises.writeFile(envFilePath, envMapToString(envMap));
return envMap;
};
/**
* Sets the value of an environment variable in the .env file
*
* @param {string} key - The key of the environment variable
* @param {string} value - The value of the environment variable
*/
export const setEnvVariable = async (key: EnvKeys, value: string) => {
const rootFolder = process.cwd();
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);
envMap.set(key, value);
await fs.promises.writeFile(envFilePath, envMapToString(envMap));
};
/**
* Copies the system files from the assets folder to the current working directory
*/
export const copySystemFiles = async () => {
// Remove old unused files
if (await pathExists(path.join(process.cwd(), 'scripts'))) {
fileLogger.info('Removing old scripts folder');
await fs.promises.rmdir(path.join(process.cwd(), 'scripts'), { recursive: true });
}
const assetsFolder = path.join('/snapshot', 'runtipi', 'packages', 'cli', 'assets');
// Copy docker-compose.yml file
fileLogger.info('Copying file docker-compose.yml');
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
fileLogger.info('Copying file VERSION');
logger.info('Copying file VERSION');
await fs.promises.copyFile(path.join(assetsFolder, 'VERSION'), path.join(process.cwd(), 'VERSION'));
// Copy traefik folder from assets
fileLogger.info('Creating traefik folders');
await fs.promises.mkdir(path.join(process.cwd(), 'traefik', 'dynamic'), { recursive: true });
await fs.promises.mkdir(path.join(process.cwd(), 'traefik', 'shared'), { recursive: true });
await fs.promises.mkdir(path.join(process.cwd(), 'traefik', 'tls'), { recursive: true });
fileLogger.info('Copying traefik files');
await fs.promises.copyFile(path.join(assetsFolder, 'traefik', 'traefik.yml'), path.join(process.cwd(), 'traefik', 'traefik.yml'));
await fs.promises.copyFile(path.join(assetsFolder, 'traefik', 'dynamic', 'dynamic.yml'), path.join(process.cwd(), 'traefik', 'dynamic', 'dynamic.yml'));
// Create base folders
fileLogger.info('Creating base folders');
await fs.promises.mkdir(path.join(process.cwd(), 'apps'), { recursive: true });
await fs.promises.mkdir(path.join(process.cwd(), 'app-data'), { recursive: true });
await fs.promises.mkdir(path.join(process.cwd(), 'state'), { recursive: true });
await fs.promises.mkdir(path.join(process.cwd(), 'repos'), { recursive: true });
// Create media folders
fileLogger.info('Creating media folders');
await fs.promises.mkdir(path.join(process.cwd(), 'media', 'torrents', 'watch'), { recursive: true });
await fs.promises.mkdir(path.join(process.cwd(), 'media', 'torrents', 'complete'), { recursive: true });
await fs.promises.mkdir(path.join(process.cwd(), 'media', 'torrents', 'incomplete'), { recursive: true });
await fs.promises.mkdir(path.join(process.cwd(), 'media', 'usenet', 'watch'), { recursive: true });
await fs.promises.mkdir(path.join(process.cwd(), 'media', 'usenet', 'complete'), { recursive: true });
await fs.promises.mkdir(path.join(process.cwd(), 'media', 'usenet', 'incomplete'), { recursive: true });
await fs.promises.mkdir(path.join(process.cwd(), 'media', 'downloads', 'watch'), { recursive: true });
await fs.promises.mkdir(path.join(process.cwd(), 'media', 'downloads', 'complete'), { recursive: true });
await fs.promises.mkdir(path.join(process.cwd(), 'media', 'downloads', 'incomplete'), { recursive: true });
await fs.promises.mkdir(path.join(process.cwd(), 'media', 'data', 'books'), { recursive: true });
await fs.promises.mkdir(path.join(process.cwd(), 'media', 'data', 'comics'), { recursive: true });
await fs.promises.mkdir(path.join(process.cwd(), 'media', 'data', 'movies'), { recursive: true });
await fs.promises.mkdir(path.join(process.cwd(), 'media', 'data', 'music'), { recursive: true });
await fs.promises.mkdir(path.join(process.cwd(), 'media', 'data', 'tv'), { recursive: true });
await fs.promises.mkdir(path.join(process.cwd(), 'media', 'data', 'podcasts'), { recursive: true });
await fs.promises.mkdir(path.join(process.cwd(), 'media', 'data', 'images'), { recursive: true });
await fs.promises.mkdir(path.join(process.cwd(), 'media', 'data', 'roms'), { recursive: true });
};
/**
* Given a domain, generates the TLS certificates for it to be used with Traefik
*
* @param {string} data.domain The domain to generate the certificates for
*/
export const generateTlsCertificates = async (data: { domain?: string }) => {
if (!data.domain) {
return;
}
// If the certificate already exists, don't generate it again
if (await pathExists(path.join(process.cwd(), 'traefik', 'tls', `${data.domain}.txt`))) {
fileLogger.info(`TLS certificate for ${data.domain} already exists`);
return;
}
// Remove old certificates
if (await pathExists(path.join(process.cwd(), 'traefik', 'tls', 'cert.pem'))) {
fileLogger.info('Removing old TLS certificate');
await fs.promises.unlink(path.join(process.cwd(), 'traefik', 'tls', 'cert.pem'));
}
if (await pathExists(path.join(process.cwd(), 'traefik', 'tls', 'key.pem'))) {
fileLogger.info('Removing old TLS key');
await fs.promises.unlink(path.join(process.cwd(), 'traefik', 'tls', 'key.pem'));
}
const subject = `/O=runtipi.io/OU=IT/CN=*.${data.domain}/emailAddress=webmaster@${data.domain}`;
const subjectAltName = `DNS:*.${data.domain},DNS:${data.domain}`;
try {
fileLogger.info(`Generating TLS certificate for ${data.domain}`);
await execAsync(`openssl req -x509 -newkey rsa:4096 -keyout traefik/tls/key.pem -out traefik/tls/cert.pem -days 365 -subj "${subject}" -addext "subjectAltName = ${subjectAltName}" -nodes`);
fileLogger.info(`Writing txt file for ${data.domain}`);
await fs.promises.writeFile(path.join(process.cwd(), 'traefik', 'tls', `${data.domain}.txt`), '');
} catch (error) {
fileLogger.error(error);
console.error(chalk.red('✗'), 'Failed to generate TLS certificates');
}
};

View file

@ -3,28 +3,20 @@ import { program } from 'commander';
import chalk from 'chalk';
import { description, version } from '../package.json';
import { startWorker } from './services/watcher/watcher';
import { SystemExecutors } from './executors';
import { AppExecutors, SystemExecutors } from './executors';
const main = async () => {
program.description(description).version(version);
program
.command('watch')
.description('Watcher script for events queue')
.action(async () => {
console.log('Starting watcher');
startWorker();
});
program.name('./runtipi-cli').usage('<command> [options]');
program
.command('start')
.description('Start tipi')
.option('--no-permissions', 'Skip permissions check')
.option('--no-sudo', 'Skip sudo usage')
.action(async (options) => {
.addHelpText('after', '\nExample call: sudo ./runtipi-cli start')
.action(async () => {
const systemExecutors = new SystemExecutors();
await systemExecutors.start(options.sudo);
await systemExecutors.start();
});
program
@ -69,6 +61,26 @@ const main = async () => {
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);
};

View file

@ -1,12 +0,0 @@
/**
* Returns the user id and group id of the current user
*/
export const getUserIds = () => {
if (process.getgid && process.getuid) {
const isSudo = process.getgid() === 0 && process.getuid() === 0;
return { uid: process.getuid(), gid: process.getgid(), isSudo };
}
return { uid: 1000, gid: 1000, isSudo: false };
};

View file

@ -1,4 +0,0 @@
import { createLogger } from '@runtipi/shared';
import path from 'path';
export const fileLogger = createLogger('cli', path.join(process.cwd(), 'logs'));

View file

@ -0,0 +1,4 @@
import { FileLogger } from '@runtipi/shared';
import path from 'node:path';
export const logger = new FileLogger('cli', path.join(process.cwd(), 'logs'));

View file

@ -11,7 +11,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"winston": "^3.9.0",
"zod": "^3.21.4"
"winston": "^3.11.0",
"zod": "^3.22.4"
}
}

View file

@ -1 +1,2 @@
export * from './env-helpers';
export * from './fs-helpers';

View file

@ -1,3 +1,5 @@
export * from './schemas';
export * from './helpers';
export { createLogger } from './utils/logger';
export { FileLogger } from './lib/FileLogger';
export { execAsync } from './lib/exec-async';

View file

@ -0,0 +1,61 @@
import fs from 'fs';
import path from 'path';
import { createLogger } from '../../utils/logger';
function streamLogToHistory(logsFolder: string, logFile: string) {
return new Promise((resolve, reject) => {
const appLogReadStream = fs.createReadStream(path.join(logsFolder, logFile), 'utf-8');
const appLogHistoryWriteStream = fs.createWriteStream(path.join(logsFolder, `${logFile}.history`), { flags: 'a' });
appLogReadStream
.pipe(appLogHistoryWriteStream)
.on('finish', () => {
fs.writeFileSync(path.join(logsFolder, logFile), '');
resolve(true);
})
.on('error', (error) => {
reject(error);
});
});
}
export class FileLogger {
private winstonLogger;
private logsFolder;
constructor(name: string, folder: string, console?: boolean) {
this.winstonLogger = createLogger(name, folder, console);
this.logsFolder = folder;
}
public flush = async () => {
try {
if (fs.existsSync(path.join(this.logsFolder, 'app.log'))) {
await streamLogToHistory(this.logsFolder, 'app.log');
}
if (fs.existsSync(path.join(this.logsFolder, 'error.log'))) {
await streamLogToHistory(this.logsFolder, 'error.log');
}
this.winstonLogger.info('Logs flushed');
} catch (error) {
this.winstonLogger.error('Error flushing logs', error);
}
};
public error = (...message: unknown[]) => {
this.winstonLogger.error(message.join(' '));
};
public info = (...message: unknown[]) => {
this.winstonLogger.info(message.join(' '));
};
public warn = (...message: unknown[]) => {
this.winstonLogger.warn(message.join(' '));
};
public debug = (...message: unknown[]) => {
this.winstonLogger.debug(message.join(' '));
};
}

View file

@ -0,0 +1 @@
export { FileLogger } from './FileLogger';

View file

@ -0,0 +1,20 @@
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: '' };
}
};

View file

@ -0,0 +1 @@
export { execAsync } from './execAsync';

View file

@ -11,7 +11,6 @@ export const envSchema = z.object({
NODE_ENV: z.union([z.literal('development'), z.literal('production'), z.literal('test')]),
REDIS_HOST: z.string(),
redisPassword: z.string(),
status: z.union([z.literal('RUNNING'), z.literal('UPDATING'), z.literal('RESTARTING')]),
architecture: z.nativeEnum(ARCHITECTURES),
dnsIp: z.string().ip().trim(),
rootFolder: z.string(),
@ -43,6 +42,14 @@ export const envSchema = z.object({
if (typeof value === 'boolean') return value;
return value === 'true';
}),
guestDashboard: z
.string()
.or(z.boolean())
.optional()
.transform((value) => {
if (typeof value === 'boolean') return value;
return value === 'true';
}),
seePreReleaseVersions: z
.string()
.or(z.boolean())
@ -51,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, appsRepoUrl: true, domain: true, storagePath: true, localDomain: true, demoMode: 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());

View file

@ -12,6 +12,7 @@ const appCommandSchema = z.object({
type: z.literal(EVENT_TYPES.APP),
command: z.union([z.literal('start'), z.literal('stop'), z.literal('install'), z.literal('uninstall'), z.literal('update'), z.literal('generate_env')]),
appid: z.string(),
skipEnv: z.boolean().optional().default(false),
form: z.object({}).catchall(z.any()),
});
@ -23,20 +24,14 @@ const repoCommandSchema = z.object({
const systemCommandSchema = z.object({
type: z.literal(EVENT_TYPES.SYSTEM),
command: z.union([z.literal('restart'), z.literal('system_info')]),
command: z.literal('system_info'),
});
const updateSchema = z.object({
type: z.literal(EVENT_TYPES.SYSTEM),
command: z.literal('update'),
version: z.string(),
});
export const eventSchema = appCommandSchema.or(repoCommandSchema).or(systemCommandSchema).or(updateSchema);
export const eventSchema = appCommandSchema.or(repoCommandSchema).or(systemCommandSchema);
export const eventResultSchema = z.object({
success: z.boolean(),
stdout: z.string(),
});
export type SystemEvent = z.infer<typeof eventSchema>;
export type SystemEvent = z.input<typeof eventSchema>;

View file

@ -12,7 +12,7 @@ type Transports = transports.ConsoleTransportInstance | transports.FileTransport
* @param {string} id - The id of the logger, used to identify the logger in the logs
* @param {string} logsFolder - The folder where the logs will be stored
*/
export const newLogger = (id: string, logsFolder: string) => {
export const newLogger = (id: string, logsFolder: string, console?: boolean) => {
if (!fs.existsSync(logsFolder)) {
fs.mkdirSync(logsFolder, { recursive: true });
}
@ -34,8 +34,10 @@ export const newLogger = (id: string, logsFolder: string) => {
);
exceptionHandlers = [new transports.File({ filename: path.join(logsFolder, 'error.log') })];
if (process.env.NODE_ENV !== 'production') {
if (process.env.NODE_ENV === 'development') {
tr.push(new transports.Console({ level: 'debug' }));
} else if (console) {
tr.push(new transports.Console({ level: 'info' }));
}
return createLogger({
@ -45,7 +47,7 @@ export const newLogger = (id: string, logsFolder: string) => {
colorize(),
timestamp(),
align(),
printf((info) => `${info.timestamp} - ${info.level} > ${info.message}`),
printf((info) => `${id}: ${info.timestamp} - ${info.level} > ${info.message}`),
),
transports: tr,
exceptionHandlers,

14
packages/worker/.env.test Normal file
View file

@ -0,0 +1,14 @@
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
REDIS_HOST=localhost
POSTGRES_HOST=localhost
POSTGRES_DBNAME=postgres
POSTGRES_USERNAME=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_PORT=5433

View file

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

2
packages/worker/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
dist/
coverage/

View file

@ -0,0 +1,6 @@
module.exports = {
singleQuote: true,
semi: true,
trailingComma: 'all',
printWidth: 200,
};

View file

@ -0,0 +1,67 @@
ARG NODE_VERSION="20.10"
ARG ALPINE_VERSION="3.18"
FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS node_base
# ---- BUILDER BASE ----
FROM node_base AS builder_base
RUN npm install pnpm -g
RUN apk add curl
# ---- RUNNER BASE ----
FROM node_base AS runner_base
RUN apk add curl openssl git && rm -rf /var/cache/apk/*
ARG NODE_ENV="production"
# ---- BUILDER ----
FROM builder_base AS builder
WORKDIR /app
ARG TARGETARCH
ENV TARGETARCH=${TARGETARCH}
RUN echo "Building for ${TARGETARCH}"
RUN if [ "${TARGETARCH}" = "arm64" ]; then \
curl -L -o docker-binary "https://github.com/docker/compose/releases/download/v2.23.1/docker-compose-linux-aarch64"; \
elif [ "${TARGETARCH}" = "amd64" ]; then \
curl -L -o docker-binary "https://github.com/docker/compose/releases/download/v2.23.1/docker-compose-linux-x86_64"; \
else \
echo "Unsupported architecture"; \
fi
RUN chmod +x docker-binary
COPY ./pnpm-lock.yaml ./
COPY ./pnpm-workspace.yaml ./
COPY ./patches ./patches
RUN pnpm fetch --no-scripts
COPY ./packages ./packages
RUN pnpm install -r --prefer-offline
COPY ./packages/worker/build.js ./packages/worker/build.js
COPY ./packages/worker/src ./packages/worker/src
COPY ./packages/worker/package.json ./packages/worker/package.json
COPY ./packages/worker/assets ./packages/worker/assets
RUN pnpm -r build --filter @runtipi/worker
# ---- RUNNER ----
FROM runner_base AS app
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/packages/worker/dist .
COPY --from=builder /app/packages/worker/assets ./assets
COPY --from=builder /app/docker-binary /usr/local/bin/docker-compose
CMD ["node", "index.js", "start"]

View file

@ -0,0 +1,41 @@
ARG NODE_VERSION="20.10"
ARG ALPINE_VERSION="3.18"
FROM node:${NODE_VERSION}-alpine${ALPINE_VERSION} AS node_base
# Install docker
RUN apk upgrade --update-cache --available && \
apk add openssl git docker docker-cli-compose curl && \
rm -rf /var/cache/apk/*
ARG TARGETARCH
ENV TARGETARCH=${TARGETARCH}
RUN echo "Building for ${TARGETARCH}"
RUN if [ "${TARGETARCH}" = "arm64" ]; then \
curl -L -o docker-binary "https://github.com/docker/compose/releases/download/v2.23.1/docker-compose-linux-aarch64"; \
elif [ "${TARGETARCH}" = "amd64" ]; then \
curl -L -o docker-binary "https://github.com/docker/compose/releases/download/v2.23.1/docker-compose-linux-x86_64"; \
fi
RUN chmod +x docker-binary
RUN mv docker-binary /usr/local/bin/docker-compose
RUN npm install pnpm -g
WORKDIR /app
COPY ./pnpm-lock.yaml ./
COPY ./pnpm-workspace.yaml ./
COPY ./patches ./patches
RUN pnpm fetch --no-scripts
COPY ./packages/worker/assets ./assets
COPY ./packages ./packages
RUN pnpm install -r --prefer-offline
CMD ["pnpm", "--filter", "@runtipi/worker", "-r", "dev"]

View file

@ -0,0 +1,15 @@
-- Update app table to add "is_visible_on_guest_dashboard" column
ALTER TABLE "app"
ADD COLUMN IF NOT EXISTS "is_visible_on_guest_dashboard" boolean DEFAULT FALSE;
-- Set default value to false
UPDATE
"app"
SET
"is_visible_on_guest_dashboard" = FALSE
WHERE
"is_visible_on_guest_dashboard" IS NULL;
-- Set is_visible_on_guest_dashboard column to not null constraint
ALTER TABLE "app"
ALTER COLUMN "is_visible_on_guest_dashboard" SET NOT NULL;

View file

@ -4,7 +4,7 @@ api:
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
endpoint: 'unix:///var/run/docker.sock'
watch: true
exposedByDefault: false
file:
@ -13,9 +13,9 @@ providers:
entryPoints:
web:
address: ":80"
address: ':80'
websecure:
address: ":443"
address: ':443'
http:
tls:
certResolver: myresolver
@ -23,7 +23,7 @@ entryPoints:
certificatesResolvers:
myresolver:
acme:
email: acme@thisprops.com
email: acme@thisprops.com
storage: /shared/acme.json
httpChallenge:
entryPoint: web

21
packages/worker/build.js Normal file
View file

@ -0,0 +1,21 @@
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();

View file

@ -0,0 +1,5 @@
{
"watch": ["src"],
"exec": "NODE_ENV=development tsx ./src/index.ts",
"ext": "js ts"
}

View file

@ -0,0 +1,42 @@
{
"name": "@runtipi/worker",
"version": "1.0.0",
"description": "",
"main": "src/index.ts",
"scripts": {
"test": "dotenv -e .env.test vitest -- --coverage --watch=false",
"test:watch": "dotenv -e .env.test vitest",
"build": "node build.js",
"tsc": "tsc",
"dev": "dotenv -e ../../.env nodemon",
"knip": "knip",
"lint": "eslint . --ext .ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@faker-js/faker": "^8.2.0",
"@types/web-push": "^3.6.3",
"dotenv-cli": "^7.3.0",
"esbuild": "^0.19.4",
"knip": "^2.41.3",
"memfs": "^4.6.0",
"nodemon": "^3.0.1",
"tsx": "^3.14.0",
"typescript": "^5.2.2",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "^0.34.6"
},
"dependencies": {
"@runtipi/postgres-migrations": "^5.3.0",
"@runtipi/shared": "workspace:^",
"bullmq": "^4.13.0",
"dotenv": "^16.3.1",
"ioredis": "^5.3.2",
"pg": "^8.11.3",
"systeminformation": "^5.21.15",
"web-push": "^3.6.6",
"zod": "^3.22.4"
}
}

View file

@ -0,0 +1,2 @@
export const ROOT_FOLDER = '/app';
export const STORAGE_FOLDER = '/storage';

View file

@ -0,0 +1 @@
export * from './constants';

View file

@ -0,0 +1,94 @@
import { SystemEvent } from '@runtipi/shared';
import http from 'node:http';
import path from 'node:path';
import Redis from 'ioredis';
import dotenv from 'dotenv';
import { Queue } from 'bullmq';
import { copySystemFiles, ensureFilePermissions, generateSystemEnvFile, generateTlsCertificates } from '@/lib/system';
import { runPostgresMigrations } from '@/lib/migrations';
import { startWorker } from './watcher/watcher';
import { logger } from '@/lib/logger';
import { AppExecutors } from './services';
const rootFolder = '/app';
const envFile = path.join(rootFolder, '.env');
const main = async () => {
try {
await logger.flush();
logger.info('Copying system files...');
await copySystemFiles();
logger.info('Generating system env file...');
const envMap = await generateSystemEnvFile();
// Reload env variables after generating the env file
logger.info('Reloading env variables...');
dotenv.config({ path: envFile, override: true });
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...');
await queue.obliterate({ force: true });
// Initial jobs
logger.info('Adding initial jobs to queue...');
await queue.add(`${Math.random().toString()}_system_info`, { type: 'system', command: 'system_info' } as SystemEvent);
await queue.add(`${Math.random().toString()}_repo_clone`, { type: 'repo', command: 'clone', url: envMap.get('APPS_REPO_URL') } as SystemEvent);
await queue.add(`${Math.random().toString()}_repo_update`, { type: 'repo', command: 'update', url: envMap.get('APPS_REPO_URL') } as SystemEvent);
// Scheduled jobs
logger.info('Adding scheduled jobs to queue...');
await queue.add(`${Math.random().toString()}_repo_update`, { type: 'repo', command: 'update', url: envMap.get('APPS_REPO_URL') } as SystemEvent, { repeat: { pattern: '*/30 * * * *' } });
await queue.add(`${Math.random().toString()}_system_info`, { type: 'system', command: 'system_info' } as SystemEvent, { repeat: { pattern: '* * * * *' } });
logger.info('Closing queue...');
await queue.close();
logger.info('Running database migrations...');
await runPostgresMigrations({
postgresHost: envMap.get('POSTGRES_HOST') as string,
postgresDatabase: envMap.get('POSTGRES_DBNAME') as string,
postgresUsername: envMap.get('POSTGRES_USERNAME') as string,
postgresPassword: envMap.get('POSTGRES_PASSWORD') as string,
postgresPort: envMap.get('POSTGRES_PORT') as string,
});
// Set status to running
logger.info('Setting status to running...');
const cache = new Redis({ host: envMap.get('REDIS_HOST'), port: 6379, password: envMap.get('REDIS_PASSWORD') });
await cache.set('status', 'RUNNING');
await cache.quit();
// Start all apps
const appExecutor = new AppExecutors();
logger.info('Starting all apps...');
appExecutor.startAllApps();
const server = http.createServer((req, res) => {
if (req.url === '/healthcheck') {
res.writeHead(200);
res.end('OK');
} else {
res.writeHead(404);
res.end('Not Found');
}
});
server.listen(3000, () => {
startWorker();
});
} catch (e) {
logger.error(e);
process.exit(1);
}
};
main();

View file

@ -0,0 +1,125 @@
// const spy = vi.spyOn(dockerHelpers, 'compose').mockImplementation(() => Promise.resolve({ stdout: '', stderr: randomError }));
import { vi, it, describe, expect } from 'vitest';
import { faker } from '@faker-js/faker';
import fs from 'fs';
import { compose } from './docker-helpers';
const execAsync = vi.fn().mockImplementation(() => Promise.resolve({ stdout: '', stderr: '' }));
vi.mock('@runtipi/shared', async (importOriginal) => {
const mod = (await importOriginal()) as object;
return {
...mod,
FileLogger: vi.fn().mockImplementation(() => ({
flush: vi.fn(),
error: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
})),
execAsync: (cmd: string) => execAsync(cmd),
};
});
describe('docker helpers', async () => {
it('should call execAsync with correct args', async () => {
// arrange
const appId = faker.word.noun().toLowerCase();
const command = faker.word.noun().toLowerCase();
// act
await compose(appId, command);
// assert
const expected = [
'docker-compose',
`--env-file /storage/app-data/${appId}/app.env`,
`--project-name ${appId}`,
`-f /app/apps/${appId}/docker-compose.yml`,
'-f /app/repos/repo-id/apps/docker-compose.common.yml',
command,
].join(' ');
expect(execAsync).toHaveBeenCalledWith(expected);
});
it('should add user env file if exists', async () => {
// arrange
const appId = faker.word.noun().toLowerCase();
const command = faker.word.noun().toLowerCase();
await fs.promises.mkdir(`/app/user-config/${appId}`, { recursive: true });
const userEnvFile = `/app/user-config/${appId}/app.env`;
await fs.promises.writeFile(userEnvFile, 'test');
// act
await compose(appId, command);
// assert
const expected = [
'docker-compose',
`--env-file /storage/app-data/${appId}/app.env`,
`--env-file ${userEnvFile}`,
`--project-name ${appId}`,
`-f /app/apps/${appId}/docker-compose.yml`,
'-f /app/repos/repo-id/apps/docker-compose.common.yml',
command,
].join(' ');
expect(execAsync).toHaveBeenCalledWith(expected);
});
it('should add user compose file if exists', async () => {
// arrange
const appId = faker.word.noun().toLowerCase();
const command = faker.word.noun().toLowerCase();
await fs.promises.mkdir(`/app/user-config/${appId}`, { recursive: true });
const userComposeFile = `/app/user-config/${appId}/docker-compose.yml`;
await fs.promises.writeFile(userComposeFile, 'test');
// act
await compose(appId, command);
// assert
const expected = [
'docker-compose',
`--env-file /storage/app-data/${appId}/app.env`,
`--project-name ${appId}`,
`-f /app/apps/${appId}/docker-compose.yml`,
'-f /app/repos/repo-id/apps/docker-compose.common.yml',
`--file ${userComposeFile}`,
command,
].join(' ');
expect(execAsync).toHaveBeenCalledWith(expected);
});
it('should add arm64 compose file if exists and arch is arm64', async () => {
// arrange
vi.mock('@/lib/environment', async (importOriginal) => {
const mod = (await importOriginal()) as object;
return { ...mod, getEnv: () => ({ arch: 'arm64', appsRepoId: 'repo-id' }) };
});
const appId = faker.word.noun().toLowerCase();
const command = faker.word.noun().toLowerCase();
await fs.promises.mkdir(`/app/apps/${appId}`, { recursive: true });
const arm64ComposeFile = `/app/apps/${appId}/docker-compose.arm64.yml`;
await fs.promises.writeFile(arm64ComposeFile, 'test');
// act
await compose(appId, command);
// assert
const expected = [
'docker-compose',
`--env-file /storage/app-data/${appId}/app.env`,
`--project-name ${appId}`,
`-f ${arm64ComposeFile}`,
`-f /app/repos/repo-id/apps/docker-compose.common.yml`,
command,
].join(' ');
expect(execAsync).toHaveBeenCalledWith(expected);
});
});

View file

@ -1,12 +1,16 @@
import path from 'path';
import { getEnv } from '../environment/environment';
import { pathExists } from '../fs-helpers/fs-helpers';
import { fileLogger } from '../logger/file-logger';
import { execAsync } from '../exec-async/execAsync';
import { execAsync, pathExists } from '@runtipi/shared';
import { logger } from '@/lib/logger';
import { getEnv } from '@/lib/environment';
import { ROOT_FOLDER, STORAGE_FOLDER } from '@/config/constants';
const composeUp = async (args: string[]) => {
fileLogger.info(`Running docker compose with args ${args.join(' ')}`);
const { stdout, stderr } = await execAsync(`docker compose ${args.join(' ')}`);
logger.info(`Running docker compose with args ${args.join(' ')}`);
const { stdout, stderr } = await execAsync(`docker-compose ${args.join(' ')}`);
if (stderr && stderr.includes('Command failed:')) {
throw new Error(stderr);
}
return { stdout, stderr };
};
@ -17,14 +21,14 @@ const composeUp = async (args: string[]) => {
* @param {string} command - Command to execute
*/
export const compose = async (appId: string, command: string) => {
const { arch, rootFolderHost, appsRepoId, storagePath } = getEnv();
const appDataDirPath = path.join(storagePath, 'app-data', appId);
const appDirPath = path.join(rootFolderHost, 'apps', appId);
const { arch, appsRepoId } = getEnv();
const appDataDirPath = path.join(STORAGE_FOLDER, 'app-data', appId);
const appDirPath = path.join(ROOT_FOLDER, 'apps', appId);
const args: string[] = [`--env-file ${path.join(appDataDirPath, 'app.env')}`];
// User custom env file
const userEnvFile = path.join(rootFolderHost, 'user-config', appId, 'app.env');
const userEnvFile = path.join(ROOT_FOLDER, 'user-config', appId, 'app.env');
if (await pathExists(userEnvFile)) {
args.push(`--env-file ${userEnvFile}`);
}
@ -37,11 +41,11 @@ export const compose = async (appId: string, command: string) => {
}
args.push(`-f ${composeFile}`);
const commonComposeFile = path.join(rootFolderHost, 'repos', appsRepoId, 'apps', 'docker-compose.common.yml');
const commonComposeFile = path.join(ROOT_FOLDER, 'repos', appsRepoId, 'apps', 'docker-compose.common.yml');
args.push(`-f ${commonComposeFile}`);
// User defined overrides
const userComposeFile = path.join(rootFolderHost, 'user-config', appId, 'docker-compose.yml');
const userComposeFile = path.join(ROOT_FOLDER, 'user-config', appId, 'docker-compose.yml');
if (await pathExists(userComposeFile)) {
args.push(`--file ${userComposeFile}`);
}

View file

@ -0,0 +1,62 @@
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(),
REDIS_HOST: z.string(),
POSTGRES_PORT: z.string(),
POSTGRES_USERNAME: z.string(),
POSTGRES_PASSWORD: z.string(),
POSTGRES_DBNAME: z.string(),
POSTGRES_HOST: z.string(),
})
.transform((env) => {
const {
STORAGE_PATH = '/app',
ARCHITECTURE,
ROOT_FOLDER_HOST,
APPS_REPO_ID,
INTERNAL_IP,
TIPI_VERSION,
REDIS_PASSWORD,
REDIS_HOST,
POSTGRES_DBNAME,
POSTGRES_PASSWORD,
POSTGRES_USERNAME,
POSTGRES_PORT,
POSTGRES_HOST,
...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,
redisHost: REDIS_HOST,
postgresPort: POSTGRES_PORT,
postgresUsername: POSTGRES_USERNAME,
postgresPassword: POSTGRES_PASSWORD,
postgresDatabase: POSTGRES_DBNAME,
postgresHost: POSTGRES_HOST,
...rest,
};
});
export const getEnv = () => environmentSchema.parse(process.env);

View file

@ -0,0 +1 @@
export { getEnv } from './environment';

View file

@ -0,0 +1 @@
export { logger } from './logger';

View file

@ -0,0 +1,4 @@
import { FileLogger } from '@runtipi/shared';
import path from 'node:path';
export const logger = new FileLogger('worker', path.join('/app', 'logs'), true);

View file

@ -0,0 +1 @@
export { runPostgresMigrations } from './run-migration';

View file

@ -1,7 +1,8 @@
import path from 'path';
import pg from 'pg';
import { migrate } from '@runtipi/postgres-migrations';
import { fileLogger } from '../logger/file-logger';
import { logger } from '@/lib/logger';
import { ROOT_FOLDER } from '@/config/constants';
type MigrationParams = {
postgresHost: string;
@ -12,13 +13,13 @@ type MigrationParams = {
};
export const runPostgresMigrations = async (params: MigrationParams) => {
const assetsFolder = path.join('/snapshot', 'runtipi', 'packages', 'cli', 'assets');
const assetsFolder = path.join(ROOT_FOLDER, 'assets');
const { postgresHost, postgresDatabase, postgresUsername, postgresPassword, postgresPort } = params;
fileLogger.info('Starting database migration');
logger.info('Starting database migration');
fileLogger.info(`Connecting to database ${postgresDatabase} on ${postgresHost} as ${postgresUsername} on port ${postgresPort}`);
logger.info(`Connecting to database ${postgresDatabase} on ${postgresHost} as ${postgresUsername} on port ${postgresPort}`);
const client = new pg.Client({
user: postgresUsername,
@ -29,28 +30,28 @@ export const runPostgresMigrations = async (params: MigrationParams) => {
});
await client.connect();
fileLogger.info('Client connected');
logger.info('Client connected');
try {
const { rows } = await client.query('SELECT * FROM migrations');
// if rows contains a migration with name 'Initial1657299198975' (legacy typeorm) delete table migrations. As all migrations are idempotent we can safely delete the table and start over.
if (rows.find((row) => row.name === 'Initial1657299198975')) {
fileLogger.info('Found legacy migration. Deleting table migrations');
logger.info('Found legacy migration. Deleting table migrations');
await client.query('DROP TABLE migrations');
}
} catch (e) {
fileLogger.info('Migrations table not found, creating it');
logger.info('Migrations table not found, creating it');
}
fileLogger.info('Running migrations');
logger.info('Running migrations');
try {
await migrate({ client }, path.join(assetsFolder, 'migrations'), { skipCreateMigrationTable: true });
} catch (e) {
fileLogger.error('Error running migrations. Dropping table migrations and trying again');
logger.error('Error running migrations. Dropping table migrations and trying again');
await client.query('DROP TABLE migrations');
await migrate({ client }, path.join(assetsFolder, 'migrations'), { skipCreateMigrationTable: true });
}
fileLogger.info('Migration complete');
logger.info('Migration complete');
await client.end();
};

View file

@ -0,0 +1 @@
export { copySystemFiles, generateSystemEnvFile, ensureFilePermissions, generateTlsCertificates } from './system.helpers';

View file

@ -0,0 +1,278 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable no-restricted-syntax */
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { envMapToString, envStringToMap, execAsync, pathExists, settingsSchema } from '@runtipi/shared';
import { logger } from '../logger/logger';
import { getRepoHash } from '../../services/repo/repo.helpers';
import { ROOT_FOLDER } from '@/config/constants';
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 & {});
const OLD_DEFAULT_REPO_URL = 'https://github.com/meienberger/runtipi-appstore';
const DEFAULT_REPO_URL = 'https://github.com/runtipi/runtipi-appstore';
/**
* Reads and returns the generated seed
*/
const getSeed = async () => {
const seedFilePath = path.join(ROOT_FOLDER, '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 () => {
if (!(await pathExists(path.join(ROOT_FOLDER, 'state', 'seed')))) {
const randomBytes = crypto.randomBytes(32);
const seed = randomBytes.toString('hex');
await fs.promises.writeFile(path.join(ROOT_FOLDER, 'state', 'seed'), seed);
}
};
/**
* 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 () => {
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'state'), { recursive: true });
const settingsFilePath = path.join(ROOT_FOLDER, 'state', 'settings.json');
const envFilePath = path.join(ROOT_FOLDER, '.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();
const { data } = settings;
if (data.appsRepoUrl === OLD_DEFAULT_REPO_URL) {
data.appsRepoUrl = DEFAULT_REPO_URL;
}
const jwtSecret = envMap.get('JWT_SECRET') || (await deriveEntropy('jwt_secret'));
const repoId = getRepoHash(data.appsRepoUrl || DEFAULT_REPO_URL);
const rootFolderHost = envMap.get('ROOT_FOLDER_HOST');
const internalIp = envMap.get('INTERNAL_IP');
if (!rootFolderHost) {
throw new Error('ROOT_FOLDER_HOST not set in .env file');
}
if (!internalIp) {
throw new Error('INTERNAL_IP not set in .env file');
}
envMap.set('APPS_REPO_ID', repoId);
envMap.set('APPS_REPO_URL', data.appsRepoUrl || DEFAULT_REPO_URL);
envMap.set('TZ', Intl.DateTimeFormat().resolvedOptions().timeZone);
envMap.set('INTERNAL_IP', data.listenIp || internalIp);
envMap.set('DNS_IP', data.dnsIp || '9.9.9.9');
envMap.set('ARCHITECTURE', getArchitecture());
envMap.set('JWT_SECRET', jwtSecret);
envMap.set('DOMAIN', data.domain || 'example.com');
envMap.set('STORAGE_PATH', data.storagePath || envMap.get('STORAGE_PATH') || rootFolderHost);
envMap.set('POSTGRES_HOST', 'tipi-db');
envMap.set('POSTGRES_DBNAME', 'tipi');
envMap.set('POSTGRES_USERNAME', 'tipi');
envMap.set('POSTGRES_PORT', String(5432));
envMap.set('REDIS_HOST', 'tipi-redis');
envMap.set('DEMO_MODE', String(data.demoMode || 'false'));
envMap.set('GUEST_DASHBOARD', String(data.guestDashboard || 'false'));
envMap.set('LOCAL_DOMAIN', data.localDomain || 'tipi.lan');
envMap.set('NODE_ENV', 'production');
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
if (await pathExists(path.join(ROOT_FOLDER, 'scripts'))) {
logger.info('Removing old scripts folder');
await fs.promises.rmdir(path.join(ROOT_FOLDER, 'scripts'), { recursive: true });
}
const assetsFolder = path.join(ROOT_FOLDER, 'assets');
// Copy traefik folder from assets
logger.info('Creating traefik folders');
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'traefik', 'dynamic'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'traefik', 'shared'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'traefik', 'tls'), { recursive: true });
logger.info('Copying traefik files');
await fs.promises.copyFile(path.join(assetsFolder, 'traefik', 'traefik.yml'), path.join(ROOT_FOLDER, 'traefik', 'traefik.yml'));
await fs.promises.copyFile(path.join(assetsFolder, 'traefik', 'dynamic', 'dynamic.yml'), path.join(ROOT_FOLDER, 'traefik', 'dynamic', 'dynamic.yml'));
// Create base folders
logger.info('Creating base folders');
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'apps'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'app-data'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'state'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'repos'), { recursive: true });
// Create media folders
logger.info('Creating media folders');
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'torrents', 'watch'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'torrents', 'complete'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'torrents', 'incomplete'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'usenet', 'watch'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'usenet', 'complete'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'usenet', 'incomplete'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'downloads', 'watch'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'downloads', 'complete'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'downloads', 'incomplete'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'data', 'books'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'data', 'comics'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'data', 'movies'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'data', 'music'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'data', 'tv'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'data', 'podcasts'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'data', 'images'), { recursive: true });
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'media', 'data', 'roms'), { recursive: true });
};
/**
* Given a domain, generates the TLS certificates for it to be used with Traefik
*
* @param {string} data.domain The domain to generate the certificates for
*/
export const generateTlsCertificates = async (data: { domain?: string }) => {
if (!data.domain) {
return;
}
// If the certificate already exists, don't generate it again
if (await pathExists(path.join(ROOT_FOLDER, 'traefik', 'tls', `${data.domain}.txt`))) {
logger.info(`TLS certificate for ${data.domain} already exists`);
return;
}
// Remove old certificates
if (await pathExists(path.join(ROOT_FOLDER, 'traefik', 'tls', 'cert.pem'))) {
logger.info('Removing old TLS certificate');
await fs.promises.unlink(path.join(ROOT_FOLDER, 'traefik', 'tls', 'cert.pem'));
}
if (await pathExists(path.join(ROOT_FOLDER, 'traefik', 'tls', 'key.pem'))) {
logger.info('Removing old TLS key');
await fs.promises.unlink(path.join(ROOT_FOLDER, 'traefik', 'tls', 'key.pem'));
}
const subject = `/O=runtipi.io/OU=IT/CN=*.${data.domain}/emailAddress=webmaster@${data.domain}`;
const subjectAltName = `DNS:*.${data.domain},DNS:${data.domain}`;
try {
logger.info(`Generating TLS certificate for ${data.domain}`);
await execAsync(`openssl req -x509 -newkey rsa:4096 -keyout traefik/tls/key.pem -out traefik/tls/cert.pem -days 365 -subj "${subject}" -addext "subjectAltName = ${subjectAltName}" -nodes`);
logger.info(`Writing txt file for ${data.domain}`);
await fs.promises.writeFile(path.join(ROOT_FOLDER, 'traefik', 'tls', `${data.domain}.txt`), '');
} catch (error) {
logger.error(error);
}
};
export const ensureFilePermissions = async () => {
const filesAndFolders = [path.join(ROOT_FOLDER, 'state'), path.join(ROOT_FOLDER, 'traefik')];
const files600 = [path.join(ROOT_FOLDER, 'traefik', 'shared', 'acme.json')];
// Give permission to read and write to all files and folders for the current user
for (const fileOrFolder of filesAndFolders) {
if (await pathExists(fileOrFolder)) {
await execAsync(`chmod -R a+rwx ${fileOrFolder}`).catch(() => {});
}
}
for (const fileOrFolder of files600) {
if (await pathExists(fileOrFolder)) {
await execAsync(`chmod 600 ${fileOrFolder}`).catch(() => {});
}
}
};

View file

@ -2,13 +2,14 @@ import fs from 'fs';
import { describe, it, expect, vi } from 'vitest';
import path from 'path';
import { faker } from '@faker-js/faker';
import { pathExists } from '@runtipi/shared';
import { AppExecutors } from '../app.executors';
import { createAppConfig } from '@/tests/apps.factory';
import * as dockerHelpers from '@/utils/docker-helpers';
import { getEnv } from '@/utils/environment/environment';
import { pathExists } from '@/utils/fs-helpers';
import * as dockerHelpers from '@/lib/docker';
import { getEnv } from '@/lib/environment';
import { ROOT_FOLDER, STORAGE_FOLDER } from '@/config/constants';
const { storagePath, rootFolderHost, appsRepoId } = getEnv();
const { appsRepoId } = getEnv();
describe('test: app executors', () => {
const appExecutors = new AppExecutors();
@ -23,7 +24,7 @@ describe('test: app executors', () => {
const { message, success } = await appExecutors.installApp(config.id, config);
// assert
const envExists = await pathExists(path.join(storagePath, 'app-data', config.id, 'app.env'));
const envExists = await pathExists(path.join(STORAGE_FOLDER, 'app-data', config.id, 'app.env'));
expect(success).toBe(true);
expect(message).toBe(`App ${config.id} installed successfully`);
@ -32,17 +33,34 @@ describe('test: app executors', () => {
spy.mockRestore();
});
it('should return error if compose script fails', async () => {
// arrange
const randomError = faker.system.fileName();
const spy = vi.spyOn(dockerHelpers, 'compose').mockImplementation(() => {
throw new Error(randomError);
});
const config = createAppConfig({}, false);
// act
const { message, success } = await appExecutors.installApp(config.id, config);
// assert
expect(success).toBe(false);
expect(message).toContain(randomError);
spy.mockRestore();
});
it('should delete existing app folder', async () => {
// arrange
const config = createAppConfig();
await fs.promises.mkdir(path.join(rootFolderHost, 'apps', config.id), { recursive: true });
await fs.promises.writeFile(path.join(rootFolderHost, 'apps', config.id, 'test.txt'), 'test');
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'apps', config.id), { recursive: true });
await fs.promises.writeFile(path.join(ROOT_FOLDER, 'apps', config.id, 'test.txt'), 'test');
// act
await appExecutors.installApp(config.id, config);
// assert
const exists = await pathExists(path.join(storagePath, 'apps', config.id, 'test.txt'));
const exists = await pathExists(path.join(STORAGE_FOLDER, 'apps', config.id, 'test.txt'));
expect(exists).toBe(false);
});
@ -51,13 +69,13 @@ describe('test: app executors', () => {
// arrange
const config = createAppConfig();
const filename = faker.system.fileName();
await fs.promises.writeFile(path.join(storagePath, 'app-data', config.id, filename), 'test');
await fs.promises.writeFile(path.join(STORAGE_FOLDER, 'app-data', config.id, filename), 'test');
// act
await appExecutors.installApp(config.id, config);
// assert
const exists = await pathExists(path.join(storagePath, 'app-data', config.id, filename));
const exists = await pathExists(path.join(STORAGE_FOLDER, 'app-data', config.id, filename));
expect(exists).toBe(true);
});
@ -66,15 +84,15 @@ describe('test: app executors', () => {
// arrange
const config = createAppConfig({}, false);
const filename = faker.system.fileName();
await fs.promises.mkdir(path.join(rootFolderHost, 'repos', appsRepoId, 'apps', config.id, 'data'), { recursive: true });
await fs.promises.writeFile(path.join(rootFolderHost, 'repos', appsRepoId, 'apps', config.id, 'data', filename), 'test');
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'repos', appsRepoId, 'apps', config.id, 'data'), { recursive: true });
await fs.promises.writeFile(path.join(ROOT_FOLDER, 'repos', appsRepoId, 'apps', config.id, 'data', filename), 'test');
// act
await appExecutors.installApp(config.id, config);
// assert
const exists = await pathExists(path.join(storagePath, 'app-data', config.id, 'data', filename));
const data = await fs.promises.readFile(path.join(storagePath, 'app-data', config.id, 'data', filename), 'utf-8');
const exists = await pathExists(path.join(STORAGE_FOLDER, 'app-data', config.id, 'data', filename));
const data = await fs.promises.readFile(path.join(STORAGE_FOLDER, 'app-data', config.id, 'data', filename), 'utf-8');
expect(exists).toBe(true);
expect(data).toBe('test');
@ -84,16 +102,16 @@ describe('test: app executors', () => {
// arrange
const config = createAppConfig();
const filename = faker.system.fileName();
await fs.promises.writeFile(path.join(storagePath, 'app-data', config.id, 'data', filename), 'test');
await fs.promises.mkdir(path.join(rootFolderHost, 'repos', appsRepoId, 'apps', config.id, 'data'), { recursive: true });
await fs.promises.writeFile(path.join(rootFolderHost, 'repos', appsRepoId, 'apps', config.id, 'data', filename), 'yeah');
await fs.promises.writeFile(path.join(STORAGE_FOLDER, 'app-data', config.id, 'data', filename), 'test');
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'repos', appsRepoId, 'apps', config.id, 'data'), { recursive: true });
await fs.promises.writeFile(path.join(ROOT_FOLDER, 'repos', appsRepoId, 'apps', config.id, 'data', filename), 'yeah');
// act
await appExecutors.installApp(config.id, config);
// assert
const exists = await pathExists(path.join(storagePath, 'app-data', config.id, 'data', filename));
const data = await fs.promises.readFile(path.join(storagePath, 'app-data', config.id, 'data', filename), 'utf-8');
const exists = await pathExists(path.join(STORAGE_FOLDER, 'app-data', config.id, 'data', filename));
const data = await fs.promises.readFile(path.join(STORAGE_FOLDER, 'app-data', config.id, 'data', filename), 'utf-8');
expect(exists).toBe(true);
expect(data).toBe('test');

View file

@ -1,20 +1,18 @@
import fs from 'fs';
import { describe, it, expect } from 'vitest';
import { faker } from '@faker-js/faker';
import { pathExists } from '@runtipi/shared';
import { copyDataDir, generateEnvFile } from '../app.helpers';
import { createAppConfig } from '@/tests/apps.factory';
import { getAppEnvMap } from '../env.helpers';
import { getEnv } from '@/utils/environment/environment';
import { pathExists } from '@/utils/fs-helpers';
const { rootFolderHost, storagePath } = getEnv();
import { ROOT_FOLDER, STORAGE_FOLDER } from '@/config/constants';
describe('app helpers', () => {
describe('Test: generateEnvFile()', () => {
it('should throw an error if the app has an invalid config.json file', async () => {
// arrange
const appConfig = createAppConfig();
await fs.promises.writeFile(`${rootFolderHost}/apps/${appConfig.id}/config.json`, '{}');
await fs.promises.writeFile(`${ROOT_FOLDER}/apps/${appConfig.id}/config.json`, '{}');
// act & assert
expect(generateEnvFile(appConfig.id, {})).rejects.toThrowError(`App ${appConfig.id} has invalid config.json file`);
@ -50,8 +48,8 @@ describe('app helpers', () => {
// arrange
const appConfig = createAppConfig({ form_fields: [{ env_variable: 'RANDOM_FIELD', type: 'random', label: 'test', min: 32, max: 32, required: true }] });
const randomField = faker.string.alphanumeric(32);
await fs.promises.mkdir(`${rootFolderHost}/app-data/${appConfig.id}`, { recursive: true });
await fs.promises.writeFile(`${rootFolderHost}/app-data/${appConfig.id}/app.env`, `RANDOM_FIELD=${randomField}`);
await fs.promises.mkdir(`${STORAGE_FOLDER}/app-data/${appConfig.id}`, { recursive: true });
await fs.promises.writeFile(`${STORAGE_FOLDER}/app-data/${appConfig.id}/app.env`, `RANDOM_FIELD=${randomField}`);
// act
await generateEnvFile(appConfig.id, {});
@ -117,7 +115,7 @@ describe('app helpers', () => {
it('Should not re-create app-data folder if it already exists', async () => {
// arrange
const appConfig = createAppConfig({});
await fs.promises.mkdir(`${rootFolderHost}/app-data/${appConfig.id}`, { recursive: true });
await fs.promises.mkdir(`${ROOT_FOLDER}/app-data/${appConfig.id}`, { recursive: true });
// act
await generateEnvFile(appConfig.id, {});
@ -161,8 +159,8 @@ describe('app helpers', () => {
const vapidPublicKey = faker.string.alphanumeric(32);
// act
await fs.promises.mkdir(`${rootFolderHost}/app-data/${appConfig.id}`, { recursive: true });
await fs.promises.writeFile(`${rootFolderHost}/app-data/${appConfig.id}/app.env`, `VAPID_PRIVATE_KEY=${vapidPrivateKey}\nVAPID_PUBLIC_KEY=${vapidPublicKey}`);
await fs.promises.mkdir(`${STORAGE_FOLDER}/app-data/${appConfig.id}`, { recursive: true });
await fs.promises.writeFile(`${STORAGE_FOLDER}/app-data/${appConfig.id}/app.env`, `VAPID_PRIVATE_KEY=${vapidPrivateKey}\nVAPID_PUBLIC_KEY=${vapidPublicKey}`);
await generateEnvFile(appConfig.id, {});
const envmap = await getAppEnvMap(appConfig.id);
@ -181,13 +179,13 @@ describe('app helpers', () => {
await copyDataDir(appConfig.id);
// assert
expect(await pathExists(`${rootFolderHost}/apps/${appConfig.id}/data`)).toBe(false);
expect(await pathExists(`${ROOT_FOLDER}/apps/${appConfig.id}/data`)).toBe(false);
});
it('should copy data dir to app-data folder', async () => {
// arrange
const appConfig = createAppConfig({});
const dataDir = `${rootFolderHost}/apps/${appConfig.id}/data`;
const dataDir = `${ROOT_FOLDER}/apps/${appConfig.id}/data`;
await fs.promises.mkdir(dataDir, { recursive: true });
await fs.promises.writeFile(`${dataDir}/test.txt`, 'test');
@ -196,14 +194,14 @@ describe('app helpers', () => {
await copyDataDir(appConfig.id);
// assert
const appDataDir = `${storagePath}/app-data/${appConfig.id}`;
const appDataDir = `${STORAGE_FOLDER}/app-data/${appConfig.id}`;
expect(await fs.promises.readFile(`${appDataDir}/data/test.txt`, 'utf8')).toBe('test');
});
it('should copy folders recursively', async () => {
// arrange
const appConfig = createAppConfig({});
const dataDir = `${rootFolderHost}/apps/${appConfig.id}/data`;
const dataDir = `${ROOT_FOLDER}/apps/${appConfig.id}/data`;
await fs.promises.mkdir(dataDir, { recursive: true });
@ -217,7 +215,7 @@ describe('app helpers', () => {
await copyDataDir(appConfig.id);
// assert
const appDataDir = `${storagePath}/app-data/${appConfig.id}`;
const appDataDir = `${STORAGE_FOLDER}/app-data/${appConfig.id}`;
expect(await fs.promises.readFile(`${appDataDir}/data/subdir/subsubdir/test.txt`, 'utf8')).toBe('test');
expect(await fs.promises.readFile(`${appDataDir}/data/test.txt`, 'utf8')).toBe('test');
});
@ -225,8 +223,8 @@ describe('app helpers', () => {
it('should replace the content of .template files with the content of the app.env file', async () => {
// arrange
const appConfig = createAppConfig({});
const dataDir = `${rootFolderHost}/apps/${appConfig.id}/data`;
const appDataDir = `${storagePath}/app-data/${appConfig.id}`;
const dataDir = `${ROOT_FOLDER}/apps/${appConfig.id}/data`;
const appDataDir = `${STORAGE_FOLDER}/app-data/${appConfig.id}`;
await fs.promises.mkdir(dataDir, { recursive: true });
await fs.promises.mkdir(appDataDir, { recursive: true });

View file

@ -0,0 +1,297 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable no-restricted-syntax */
import fs from 'fs';
import path from 'path';
import pg from 'pg';
import { execAsync, pathExists } from '@runtipi/shared';
import { copyDataDir, generateEnvFile } from './app.helpers';
import { logger } from '@/lib/logger';
import { compose } from '@/lib/docker';
import { getEnv } from '@/lib/environment';
import { ROOT_FOLDER, STORAGE_FOLDER } from '@/config/constants';
const getDbClient = async () => {
const { postgresHost, postgresDatabase, postgresUsername, postgresPassword, postgresPort } = getEnv();
const client = new pg.Client({
host: postgresHost,
database: postgresDatabase,
user: postgresUsername,
password: postgresPassword,
port: Number(postgresPort),
});
await client.connect();
return client;
};
export class AppExecutors {
private readonly logger;
constructor() {
this.logger = logger;
}
private handleAppError = (err: unknown) => {
if (err instanceof Error) {
this.logger.error(`An error occurred: ${err.message}`);
return { success: false, message: err.message };
}
return { success: false, message: `An error occurred: ${err}` };
};
private getAppPaths = (appId: string) => {
const { appsRepoId } = getEnv();
const appDataDirPath = path.join(STORAGE_FOLDER, 'app-data', appId);
const appDirPath = path.join(ROOT_FOLDER, 'apps', appId);
const configJsonPath = path.join(appDirPath, 'config.json');
const repoPath = path.join(ROOT_FOLDER, 'repos', appsRepoId, 'apps', appId);
return { appDataDirPath, appDirPath, configJsonPath, repoPath };
};
/**
* Given an app id, ensures that the app folder exists in the apps folder
* If not, copies the app folder from the repo
* @param {string} appId - App id
*/
private ensureAppDir = async (appId: string) => {
const { appDirPath, repoPath } = this.getAppPaths(appId);
const dockerFilePath = path.join(ROOT_FOLDER, 'apps', appId, 'docker-compose.yml');
if (!(await pathExists(dockerFilePath))) {
// delete eventual app folder if exists
this.logger.info(`Deleting app ${appId} folder if exists`);
await fs.promises.rm(appDirPath, { recursive: true, force: true });
// Copy app folder from repo
this.logger.info(`Copying app ${appId} from repo ${getEnv().appsRepoId}`);
await fs.promises.cp(repoPath, appDirPath, { recursive: true });
}
};
/**
* Install an app from the repo
* @param {string} appId - The id of the app to install
* @param {Record<string, unknown>} config - The config of the app
*/
public installApp = async (appId: string, config: Record<string, unknown>) => {
try {
if (process.getuid && process.getgid) {
this.logger.info(`Installing app ${appId} as User ID: ${process.getuid()}, Group ID: ${process.getgid()}`);
} else {
this.logger.info(`Installing app ${appId}. No User ID or Group ID found.`);
}
const { appsRepoId } = getEnv();
const { appDirPath, repoPath, appDataDirPath } = this.getAppPaths(appId);
// Check if app exists in repo
const apps = await fs.promises.readdir(path.join(ROOT_FOLDER, 'repos', appsRepoId, 'apps'));
if (!apps.includes(appId)) {
this.logger.error(`App ${appId} not found in repo ${appsRepoId}`);
return { success: false, message: `App ${appId} not found in repo ${appsRepoId}` };
}
// Delete app folder if exists
this.logger.info(`Deleting folder ${appDirPath} if exists`);
await fs.promises.rm(appDirPath, { recursive: true, force: true });
// Create app folder
this.logger.info(`Creating folder ${appDirPath}`);
await fs.promises.mkdir(appDirPath, { recursive: true });
// Copy app folder from repo
this.logger.info(`Copying folder ${repoPath} to ${appDirPath}`);
await fs.promises.cp(repoPath, appDirPath, { recursive: true });
// Create folder app-data folder
this.logger.info(`Creating folder ${appDataDirPath}`);
await fs.promises.mkdir(appDataDirPath, { recursive: true });
// Create app.env file
this.logger.info(`Creating app.env file for app ${appId}`);
await generateEnvFile(appId, config);
// Copy data dir
this.logger.info(`Copying data dir for app ${appId}`);
if (!(await pathExists(`${appDataDirPath}/data`))) {
await copyDataDir(appId);
}
await execAsync(`chmod -R a+rwx ${path.join(appDataDirPath)}`).catch(() => {
this.logger.error(`Error setting permissions for app ${appId}`);
});
// run docker-compose up
this.logger.info(`Running docker-compose up for app ${appId}`);
await compose(appId, 'up -d');
this.logger.info(`Docker-compose up for app ${appId} finished`);
return { success: true, message: `App ${appId} installed successfully` };
} catch (err) {
return this.handleAppError(err);
}
};
/**
* Stops an app
* @param {string} appId - The id of the app to stop
* @param {Record<string, unknown>} config - The config of the app
*/
public stopApp = async (appId: string, config: Record<string, unknown>, skipEnvGeneration = false) => {
try {
this.logger.info(`Stopping app ${appId}`);
await this.ensureAppDir(appId);
if (!skipEnvGeneration) {
this.logger.info(`Regenerating app.env file for app ${appId}`);
await generateEnvFile(appId, config);
}
await compose(appId, 'rm --force --stop');
this.logger.info(`App ${appId} stopped`);
return { success: true, message: `App ${appId} stopped successfully` };
} catch (err) {
return this.handleAppError(err);
}
};
public startApp = async (appId: string, config: Record<string, unknown>, skipEnvGeneration = false) => {
try {
const { appDataDirPath } = this.getAppPaths(appId);
this.logger.info(`Starting app ${appId}`);
await this.ensureAppDir(appId);
if (!skipEnvGeneration) {
this.logger.info(`Regenerating app.env file for app ${appId}`);
await generateEnvFile(appId, config);
}
await compose(appId, 'up --detach --force-recreate --remove-orphans --pull always');
this.logger.info(`App ${appId} started`);
this.logger.info(`Setting permissions for app ${appId}`);
await execAsync(`chmod -R a+rwx ${path.join(appDataDirPath)}`).catch(() => {
this.logger.error(`Error setting permissions for app ${appId}`);
});
return { success: true, message: `App ${appId} started successfully` };
} catch (err) {
return this.handleAppError(err);
}
};
public uninstallApp = async (appId: string, config: Record<string, unknown>) => {
try {
const { appDirPath, appDataDirPath } = this.getAppPaths(appId);
this.logger.info(`Uninstalling app ${appId}`);
this.logger.info(`Regenerating app.env file for app ${appId}`);
await this.ensureAppDir(appId);
await generateEnvFile(appId, config);
try {
await compose(appId, 'down --remove-orphans --volumes --rmi all');
} catch (err) {
if (err instanceof Error && err.message.includes('conflict')) {
this.logger.warn(`Could not fully uninstall app ${appId}. Some images are in use by other apps. Consider cleaning unused images docker system prune -a`);
} else {
throw err;
}
}
this.logger.info(`Deleting folder ${appDirPath}`);
await fs.promises.rm(appDirPath, { recursive: true, force: true }).catch((err) => {
this.logger.error(`Error deleting folder ${appDirPath}: ${err.message}`);
});
this.logger.info(`Deleting folder ${appDataDirPath}`);
await fs.promises.rm(appDataDirPath, { recursive: true, force: true }).catch((err) => {
this.logger.error(`Error deleting folder ${appDataDirPath}: ${err.message}`);
});
this.logger.info(`App ${appId} uninstalled`);
return { success: true, message: `App ${appId} uninstalled successfully` };
} catch (err) {
return this.handleAppError(err);
}
};
public updateApp = async (appId: string, config: Record<string, unknown>) => {
try {
const { appDirPath, repoPath } = this.getAppPaths(appId);
this.logger.info(`Updating app ${appId}`);
await this.ensureAppDir(appId);
await generateEnvFile(appId, config);
await compose(appId, 'up --detach --force-recreate --remove-orphans');
await compose(appId, 'down --rmi all --remove-orphans');
this.logger.info(`Deleting folder ${appDirPath}`);
await fs.promises.rm(appDirPath, { recursive: true, force: true });
this.logger.info(`Copying folder ${repoPath} to ${appDirPath}`);
await fs.promises.cp(repoPath, appDirPath, { recursive: true });
await compose(appId, 'pull');
return { success: true, message: `App ${appId} updated successfully` };
} catch (err) {
return this.handleAppError(err);
}
};
public regenerateAppEnv = async (appId: string, config: Record<string, unknown>) => {
try {
this.logger.info(`Regenerating app.env file for app ${appId}`);
await this.ensureAppDir(appId);
await generateEnvFile(appId, config);
return { success: true, message: `App ${appId} env file regenerated successfully` };
} catch (err) {
return this.handleAppError(err);
}
};
/**
* Start all apps with status running
*/
public startAllApps = async () => {
const client = await getDbClient();
try {
// Get all apps with status running
const { rows } = await client.query(`SELECT * FROM app WHERE status = 'running'`);
// Update all apps with status different than running or stopped to stopped
await client.query(`UPDATE app SET status = 'stopped' WHERE status != 'stopped' AND status != 'running' AND status != 'missing'`);
// Start all apps
for (const row of rows) {
const { id, config } = row;
const { success } = await this.startApp(id, config);
if (!success) {
this.logger.error(`Error starting app ${id}`);
await client.query(`UPDATE app SET status = 'stopped' WHERE id = '${id}'`);
} else {
await client.query(`UPDATE app SET status = 'running' WHERE id = '${id}'`);
}
}
} catch (err) {
this.logger.error(`Error starting apps: ${err}`);
} finally {
await client.end();
}
};
}

View file

@ -1,11 +1,10 @@
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import { appInfoSchema, envMapToString, envStringToMap } from '@runtipi/shared';
import { getEnv } from '@/utils/environment/environment';
import { appInfoSchema, envMapToString, envStringToMap, execAsync, pathExists } from '@runtipi/shared';
import { generateVapidKeys, getAppEnvMap } from './env.helpers';
import { pathExists } from '@/utils/fs-helpers';
import { execAsync } from '@/utils/exec-async/execAsync';
import { getEnv } from '@/lib/environment';
import { ROOT_FOLDER, STORAGE_FOLDER } from '@/config/constants';
/**
* This function generates a random string of the provided length by using the SHA-256 hash algorithm.
@ -17,7 +16,7 @@ import { execAsync } from '@/utils/exec-async/execAsync';
*/
const getEntropy = async (name: string, length: number) => {
const hash = crypto.createHash('sha256');
const seed = await fs.promises.readFile(path.join(getEnv().rootFolderHost, 'state', 'seed'));
const seed = await fs.promises.readFile(path.join(ROOT_FOLDER, 'state', 'seed'));
hash.update(name + seed.toString());
return hash.digest('hex').substring(0, length);
@ -36,16 +35,16 @@ const getEntropy = async (name: string, length: number) => {
* @throws Will throw an error if the app has an invalid config.json file or if a required variable is missing.
*/
export const generateEnvFile = async (appId: string, config: Record<string, unknown>) => {
const { rootFolderHost, storagePath, internalIp } = getEnv();
const { internalIp, storagePath, rootFolderHost } = getEnv();
const configFile = await fs.promises.readFile(path.join(rootFolderHost, 'apps', appId, 'config.json'));
const configFile = await fs.promises.readFile(path.join(ROOT_FOLDER, 'apps', appId, 'config.json'));
const parsedConfig = appInfoSchema.safeParse(JSON.parse(configFile.toString()));
if (!parsedConfig.success) {
throw new Error(`App ${appId} has invalid config.json file`);
}
const baseEnvFile = await fs.promises.readFile(path.join(rootFolderHost, '.env'));
const baseEnvFile = await fs.promises.readFile(path.join(ROOT_FOLDER, '.env'));
const envMap = envStringToMap(baseEnvFile.toString());
// Default always present env variables
@ -97,15 +96,16 @@ export const generateEnvFile = async (appId: string, config: Record<string, unkn
} else {
envMap.set('APP_DOMAIN', `${internalIp}:${parsedConfig.data.port}`);
envMap.set('APP_HOST', internalIp);
envMap.set('APP_PROTOCOL', 'http');
}
// Create app-data folder if it doesn't exist
const appDataDirectoryExists = await fs.promises.stat(path.join(storagePath, 'app-data', appId)).catch(() => false);
const appDataDirectoryExists = await fs.promises.stat(path.join(STORAGE_FOLDER, 'app-data', appId)).catch(() => false);
if (!appDataDirectoryExists) {
await fs.promises.mkdir(path.join(storagePath, 'app-data', appId), { recursive: true });
await fs.promises.mkdir(path.join(STORAGE_FOLDER, 'app-data', appId), { recursive: true });
}
await fs.promises.writeFile(path.join(storagePath, 'app-data', appId, 'app.env'), envMapToString(envMap));
await fs.promises.writeFile(path.join(STORAGE_FOLDER, 'app-data', appId, 'app.env'), envMapToString(envMap));
};
/**
@ -132,40 +132,38 @@ const renderTemplate = (template: string, envMap: Map<string, string>) => {
* @param {string} id - The id of the app.
*/
export const copyDataDir = async (id: string) => {
const { rootFolderHost, storagePath } = getEnv();
const envMap = await getAppEnvMap(id);
// return if app does not have a data directory
if (!(await pathExists(`${rootFolderHost}/apps/${id}/data`))) {
if (!(await pathExists(`${ROOT_FOLDER}/apps/${id}/data`))) {
return;
}
// Create app-data folder if it doesn't exist
if (!(await pathExists(`${storagePath}/app-data/${id}/data`))) {
await fs.promises.mkdir(`${storagePath}/app-data/${id}/data`, { recursive: true });
if (!(await pathExists(`${STORAGE_FOLDER}/app-data/${id}/data`))) {
await fs.promises.mkdir(`${STORAGE_FOLDER}/app-data/${id}/data`, { recursive: true });
}
const dataDir = await fs.promises.readdir(`${rootFolderHost}/apps/${id}/data`);
const dataDir = await fs.promises.readdir(`${ROOT_FOLDER}/apps/${id}/data`);
const processFile = async (file: string) => {
if (file.endsWith('.template')) {
const template = await fs.promises.readFile(`${rootFolderHost}/apps/${id}/data/${file}`, 'utf-8');
const template = await fs.promises.readFile(`${ROOT_FOLDER}/apps/${id}/data/${file}`, 'utf-8');
const renderedTemplate = renderTemplate(template, envMap);
await fs.promises.writeFile(`${storagePath}/app-data/${id}/data/${file.replace('.template', '')}`, renderedTemplate);
await fs.promises.writeFile(`${STORAGE_FOLDER}/app-data/${id}/data/${file.replace('.template', '')}`, renderedTemplate);
} else {
await fs.promises.copyFile(`${rootFolderHost}/apps/${id}/data/${file}`, `${storagePath}/app-data/${id}/data/${file}`);
await fs.promises.copyFile(`${ROOT_FOLDER}/apps/${id}/data/${file}`, `${STORAGE_FOLDER}/app-data/${id}/data/${file}`);
}
};
const processDir = async (p: string) => {
await fs.promises.mkdir(`${storagePath}/app-data/${id}/data/${p}`, { recursive: true });
const files = await fs.promises.readdir(`${rootFolderHost}/apps/${id}/data/${p}`);
await fs.promises.mkdir(`${STORAGE_FOLDER}/app-data/${id}/data/${p}`, { recursive: true });
const files = await fs.promises.readdir(`${ROOT_FOLDER}/apps/${id}/data/${p}`);
await Promise.all(
files.map(async (file) => {
const fullPath = `${rootFolderHost}/apps/${id}/data/${p}/${file}`;
const fullPath = `${ROOT_FOLDER}/apps/${id}/data/${p}/${file}`;
if ((await fs.promises.lstat(fullPath)).isDirectory()) {
await processDir(`${p}/${file}`);
@ -178,7 +176,7 @@ export const copyDataDir = async (id: string) => {
await Promise.all(
dataDir.map(async (file) => {
const fullPath = `${rootFolderHost}/apps/${id}/data/${file}`;
const fullPath = `${ROOT_FOLDER}/apps/${id}/data/${file}`;
if ((await fs.promises.lstat(fullPath)).isDirectory()) {
await processDir(file);
@ -189,7 +187,7 @@ export const copyDataDir = async (id: string) => {
);
// Remove any .gitkeep files from the app-data folder at any level
if (await pathExists(`${storagePath}/app-data/${id}/data`)) {
await execAsync(`find ${storagePath}/app-data/${id}/data -name .gitkeep -delete`).catch(() => {});
if (await pathExists(`${STORAGE_FOLDER}/app-data/${id}/data`)) {
await execAsync(`find ${STORAGE_FOLDER}/app-data/${id}/data -name .gitkeep -delete`).catch(() => {});
}
};

View file

@ -1,7 +1,7 @@
import webpush from 'web-push';
import fs from 'fs';
import path from 'path';
import { getEnv } from '@/utils/environment/environment';
import { STORAGE_FOLDER } from '@/config/constants';
/**
* This function reads the env file for the app with the provided id and returns a Map containing the key-value pairs of the environment variables.
@ -11,7 +11,7 @@ import { getEnv } from '@/utils/environment/environment';
*/
export const getAppEnvMap = async (appId: string) => {
try {
const envFile = await fs.promises.readFile(path.join(getEnv().storagePath, 'app-data', appId, 'app.env'));
const envFile = await fs.promises.readFile(path.join(STORAGE_FOLDER, 'app-data', appId, 'app.env'));
const envVars = envFile.toString().split('\n');
const envVarsMap = new Map<string, string>();

View file

@ -0,0 +1,3 @@
export { AppExecutors } from './app/app.executors';
export { RepoExecutors } from './repo/repo.executors';
export { SystemExecutors } from './system/system.executors';

View file

@ -1,15 +1,13 @@
import { getEnv } from 'src/utils/environment/environment';
import path from 'path';
import { pathExists } from '@/utils/fs-helpers';
import { getRepoHash } from './repo.helpers';
import { fileLogger } from '@/utils/logger/file-logger';
import { execAsync } from '@/utils/exec-async/execAsync';
import { execAsync, pathExists } from '@runtipi/shared';
import { getRepoHash, getRepoBaseUrlAndBranch } from './repo.helpers';
import { logger } from '@/lib/logger';
export class RepoExecutors {
private readonly logger;
constructor() {
this.logger = fileLogger;
this.logger = logger;
}
/**
@ -28,23 +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 { rootFolderHost } = getEnv();
const repoHash = getRepoHash(repoUrl);
const repoPath = path.join(rootFolderHost, 'repos', repoHash);
// 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: '' };
@ -60,10 +67,8 @@ export class RepoExecutors {
*/
public pullRepo = async (repoUrl: string) => {
try {
const { rootFolderHost } = getEnv();
const repoHash = getRepoHash(repoUrl);
const repoPath = path.join(rootFolderHost, 'repos', repoHash);
const repoPath = path.join('/app', 'repos', repoHash);
if (!(await pathExists(repoPath))) {
this.logger.info(`Repo ${repoUrl} does not exist`);
@ -93,11 +98,7 @@ export class RepoExecutors {
});
this.logger.info(`Executing: git -C ${repoPath} fetch origin && git -C ${repoPath} reset --hard origin/${currentBranch}`);
await execAsync(`git -C ${repoPath} fetch origin && git -C ${repoPath} reset --hard origin/${currentBranch}`).then(({ stderr }) => {
if (stderr) {
this.logger.error(`stderr: ${stderr}`);
}
});
await execAsync(`git -C ${repoPath} fetch origin && git -C ${repoPath} reset --hard origin/${currentBranch}`);
this.logger.info(`Pulled repo ${repoUrl} to ${repoPath}`);
return { success: true, message: '' };

View file

@ -0,0 +1,27 @@
import crypto from 'crypto';
/**
* Given a repo url, return a hash of it to be used as a folder name
*
* @param {string} repoUrl
*/
export const getRepoHash = (repoUrl: string) => {
const hash = crypto.createHash('sha256');
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] ;
};

View file

@ -0,0 +1,60 @@
import fs from 'fs';
import path from 'path';
import si from 'systeminformation';
import { logger } from '@/lib/logger';
import { ROOT_FOLDER } from '@/config/constants';
export class SystemExecutors {
private readonly logger;
constructor() {
this.logger = logger;
}
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}` };
};
private getSystemLoad = async () => {
const { currentLoad } = await si.currentLoad();
const memResult = { total: 0, used: 0, available: 0 };
try {
const memInfo = await fs.promises.readFile('/host/proc/meminfo');
memResult.total = Number(memInfo.toString().match(/MemTotal:\s+(\d+)/)?.[1] ?? 0) * 1024;
memResult.available = Number(memInfo.toString().match(/MemAvailable:\s+(\d+)/)?.[1] ?? 0) * 1024;
memResult.used = memResult.total - memResult.available;
} catch (e) {
this.logger.error(`Unable to read /host/proc/meminfo: ${e}`);
}
const [disk0] = await si.fsSize();
return {
cpu: { load: currentLoad },
memory: memResult,
disk: { total: disk0?.size, used: disk0?.used, available: disk0?.available },
};
};
public systemInfo = async () => {
try {
const systemLoad = await this.getSystemLoad();
await fs.promises.writeFile(path.join(ROOT_FOLDER, 'state', 'system-info.json'), JSON.stringify(systemLoad, null, 2));
await fs.promises.chmod(path.join(ROOT_FOLDER, 'state', 'system-info.json'), 0o777);
return { success: true, message: '' };
} catch (e) {
return this.handleSystemError(e);
}
};
}

View file

@ -1,18 +1,13 @@
import { eventSchema } from '@runtipi/shared';
import { Worker } from 'bullmq';
import { AppExecutors, RepoExecutors, SystemExecutors } from '@/executors';
import { getEnv } from '@/utils/environment/environment';
import { getUserIds } from '@/utils/environment/user';
import { fileLogger } from '@/utils/logger/file-logger';
import { execAsync } from '@/utils/exec-async/execAsync';
import { AppExecutors, RepoExecutors, SystemExecutors } from '@/services';
import { logger } from '@/lib/logger';
import { getEnv } from '@/lib/environment';
const runCommand = async (jobData: unknown) => {
const { gid, uid } = getUserIds();
fileLogger.info(`Running command with uid ${uid} and gid ${gid}`);
const { installApp, startApp, stopApp, uninstallApp, updateApp, regenerateAppEnv } = new AppExecutors();
const { cloneRepo, pullRepo } = new RepoExecutors();
const { systemInfo, restart, update } = new SystemExecutors();
const { systemInfo } = new SystemExecutors();
const event = eventSchema.safeParse(jobData);
@ -31,11 +26,11 @@ const runCommand = async (jobData: unknown) => {
}
if (data.command === 'stop') {
({ success, message } = await stopApp(data.appid, data.form));
({ success, message } = await stopApp(data.appid, data.form, data.skipEnv));
}
if (data.command === 'start') {
({ success, message } = await startApp(data.appid, data.form));
({ success, message } = await startApp(data.appid, data.form, data.skipEnv));
}
if (data.command === 'uninstall') {
@ -61,38 +56,11 @@ const runCommand = async (jobData: unknown) => {
if (data.command === 'system_info') {
({ success, message } = await systemInfo());
}
if (data.command === 'restart') {
({ success, message } = await restart());
}
if (data.command === 'update') {
({ success, message } = await update(data.version));
}
}
return { success, message };
};
export const killOtherWorkers = async () => {
const { stdout } = await execAsync('ps aux | grep "index.js watch" | grep -v grep | awk \'{print $2}\'');
const { stdout: stdoutInherit } = await execAsync('ps aux | grep "runtipi-cli watch" | grep -v grep | awk \'{print $2}\'');
fileLogger.info(`Killing other workers with pids ${stdout} and ${stdoutInherit}`);
const pids = stdout.split('\n').filter((pid: string) => pid !== '');
const pidsInherit = stdoutInherit.split('\n').filter((pid: string) => pid !== '');
pids.concat(pidsInherit).forEach((pid) => {
fileLogger.info(`Killing worker with pid ${pid}`);
try {
process.kill(Number(pid));
} catch (e) {
fileLogger.error(`Error killing worker with pid ${pid}: ${e}`);
}
});
};
/**
* Start the worker for the events queue
*/
@ -100,27 +68,27 @@ export const startWorker = async () => {
const worker = new Worker(
'events',
async (job) => {
fileLogger.info(`Processing job ${job.id} with data ${JSON.stringify(job.data)}`);
logger.info(`Processing job ${job.id} with data ${JSON.stringify(job.data)}`);
const { message, success } = await runCommand(job.data);
return { success, stdout: message };
},
{ connection: { host: '127.0.0.1', port: 6379, password: getEnv().redisPassword, connectTimeout: 60000 }, removeOnComplete: { count: 200 }, removeOnFail: { count: 500 } },
{ connection: { host: getEnv().redisHost, port: 6379, password: getEnv().redisPassword, connectTimeout: 60000 }, removeOnComplete: { count: 200 }, removeOnFail: { count: 500 } },
);
worker.on('ready', () => {
fileLogger.info('Worker is ready');
logger.info('Worker is ready');
});
worker.on('completed', (job) => {
fileLogger.info(`Job ${job.id} completed with result: ${JSON.stringify(job.returnvalue)}`);
logger.info(`Job ${job.id} completed with result:`, JSON.stringify(job.returnvalue));
});
worker.on('failed', (job) => {
fileLogger.error(`Job ${job?.id} failed with reason ${job?.failedReason}`);
logger.error(`Job ${job?.id} failed with reason ${job?.failedReason}`);
});
worker.on('error', async (e) => {
fileLogger.debug(`Worker error: ${e}`);
logger.debug(`Worker error: ${e}`);
});
};

View file

@ -0,0 +1,38 @@
import { faker } from '@faker-js/faker';
import fs from 'fs';
import { APP_CATEGORIES, AppInfo, appInfoSchema } from '@runtipi/shared';
import { ROOT_FOLDER, STORAGE_FOLDER } from '@/config/constants';
export const createAppConfig = (props?: Partial<AppInfo>, isInstalled = true) => {
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[`${ROOT_FOLDER}/.env`] = 'TEST=test';
mockFiles[`${ROOT_FOLDER}/repos/repo-id/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfoSchema.parse(appInfo));
mockFiles[`${ROOT_FOLDER}/repos/repo-id/apps/${appInfo.id}/docker-compose.yml`] = 'compose';
mockFiles[`${ROOT_FOLDER}/repos/repo-id/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
if (isInstalled) {
mockFiles[`${ROOT_FOLDER}/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfoSchema.parse(appInfo));
mockFiles[`${ROOT_FOLDER}/apps/${appInfo.id}/docker-compose.yml`] = 'compose';
mockFiles[`${ROOT_FOLDER}/apps/${appInfo.id}/metadata/description.md`] = 'md desc';
mockFiles[`${STORAGE_FOLDER}/app-data/${appInfo.id}/data/test.txt`] = 'data';
}
// @ts-expect-error - custom mock method
fs.__applyMockFiles(mockFiles);
return appInfo;
};

View file

@ -0,0 +1,41 @@
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()),
},
};

View file

@ -0,0 +1,42 @@
import fs from 'fs';
import path from 'path';
import { vi, beforeEach } from 'vitest';
import { getEnv } from '@/lib/environment';
import { ROOT_FOLDER } from '@/config/constants';
vi.mock('@runtipi/shared', async (importOriginal) => {
const mod = (await importOriginal()) as object;
return {
...mod,
createLogger: vi.fn().mockReturnValue({
info: vi.fn(),
error: vi.fn(),
}),
FileLogger: vi.fn().mockImplementation(() => ({
flush: vi.fn(),
error: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
debug: 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 { appsRepoId } = getEnv();
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'state'), { recursive: true });
await fs.promises.writeFile(path.join(ROOT_FOLDER, 'state', 'seed'), 'seed');
await fs.promises.mkdir(path.join(ROOT_FOLDER, 'repos', appsRepoId, 'apps'), { recursive: true });
});

View file

@ -0,0 +1,55 @@
{
"compilerOptions": {
"target": "es2017",
"baseUrl": ".",
"outDir": "./dist",
"paths": {
"@/lib/*": [
"./src/lib/*"
],
"@/services": [
"./src/services"
],
"@/config/*": [
"./src/config/*"
],
"@/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"
]
}

View file

@ -0,0 +1,10 @@
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'] },
},
});

4071
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

BIN
public/tipi-christmas.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View file

@ -74,6 +74,7 @@ function install_generic() {
function install_docker() {
local os="${1}"
echo "Installing docker for os ${os}"
echo "Your sudo password might be asked to install docker"
if [[ "${os}" == "debian" ]]; then
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl gnupg lsb-release
@ -120,20 +121,28 @@ 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
fi
# 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() {
@ -185,18 +194,4 @@ fi
curl --location "$URL" -o ./runtipi-cli
chmod +x ./runtipi-cli
# Check if user is in docker group
if [ "$(id -u)" -ne 0 ]; then
if ! groups | grep -q docker; then
sudo usermod -aG docker "$USER"
newgrp docker
fi
fi
# Check if git is installed
if ! command -v git >/dev/null; then
echo "Git is not installed. Please install git and restart the script."
exit 1
fi
./runtipi-cli start

View file

@ -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={{

View file

@ -36,8 +36,8 @@ export function LoginContainer() {
});
if (totpSessionId) {
return <TotpForm loading={verifyTotpMutation.isExecuting} onSubmit={(totpCode) => verifyTotpMutation.execute({ totpCode, totpSessionId })} />;
return <TotpForm loading={verifyTotpMutation.status === 'executing'} onSubmit={(totpCode) => verifyTotpMutation.execute({ totpCode, totpSessionId })} />;
}
return <LoginForm loading={loginMutation.isExecuting} onSubmit={({ email, password }) => loginMutation.execute({ username: email, password })} />;
return <LoginForm loading={loginMutation.status === 'executing'} onSubmit={({ email, password }) => loginMutation.execute({ username: email, password })} />;
}

Some files were not shown because too many files have changed in this diff Show more