diff --git a/README.md b/README.md index 1db05ac..41d64e0 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,31 @@ -# robin-runtipi-store +# Example App Store Template -personal runtipi store \ No newline at end of file +This repository serves as a template for creating your own custom app store for the Runtipi platform. Use this as a starting point to create and share your own collection of applications. + +## Repository Structure + +- **apps/**: Contains individual app directories + + - Each app has its own folder (e.g., `whoami/`) with the following structure: + - `config.json`: App configuration file + - `docker-compose.json`: Docker setup for the app + - `metadata/`: Contains app visuals and descriptions + - `description.md`: Markdown description of the app + - `logo.jpg`: App logo image + +- **tests/**: Contains test files for the app store + + - `apps.test.ts`: Test suite for validating apps + +## Getting Started + +This repository is intended to serve as a template for creating your own app store. Follow these steps to get started: + +1. Click the "Use this template" button to create a new repository based on this template +2. Customize the apps or add your own app folders in the `apps/` directory +3. Test your app store by using it with Runtipi + +## Documentation + +For detailed instructions on creating your own app store, please refer to the official guide: +[Create Your Own App Store Guide](https://runtipi.io/docs/guides/create-your-own-app-store) diff --git a/__tests__/apps.test.ts b/__tests__/apps.test.ts new file mode 100644 index 0000000..4fd30c9 --- /dev/null +++ b/__tests__/apps.test.ts @@ -0,0 +1,77 @@ +import { expect, test, describe } from "bun:test"; +import { appInfoSchema, dynamicComposeSchema } from '@runtipi/common/schemas' +import { fromError } from 'zod-validation-error'; +import fs from 'node:fs' +import path from 'node:path' + +const getApps = async () => { + const appsDir = await fs.promises.readdir(path.join(process.cwd(), 'apps')) + + const appDirs = appsDir.filter((app) => { + const stat = fs.statSync(path.join(process.cwd(), 'apps', app)) + return stat.isDirectory() + }) + + return appDirs +}; + +const getFile = async (app: string, file: string) => { + const filePath = path.join(process.cwd(), 'apps', app, file) + try { + const file = await fs.promises.readFile(filePath, 'utf-8') + return file + } catch (err) { + return null + } +} + +describe("each app should have the required files", async () => { + const apps = await getApps() + + for (const app of apps) { + const files = ['config.json', 'docker-compose.json', 'metadata/logo.jpg', 'metadata/description.md'] + + for (const file of files) { + test(`app ${app} should have ${file}`, async () => { + const fileContent = await getFile(app, file) + expect(fileContent).not.toBeNull() + }) + } + } +}) + +describe("each app should have a valid config.json", async () => { + const apps = await getApps() + + for (const app of apps) { + test(`app ${app} should have a valid config.json`, async () => { + const fileContent = await getFile(app, 'config.json') + const parsed = appInfoSchema.omit({ urn: true }).safeParse(JSON.parse(fileContent || '{}')) + + if (!parsed.success) { + const validationError = fromError(parsed.error); + console.error(`Error parsing config.json for app ${app}:`, validationError.toString()); + } + + expect(parsed.success).toBe(true) + }) + } +}) + +describe("each app should have a valid docker-compose.json", async () => { + const apps = await getApps() + + for (const app of apps) { + test(`app ${app} should have a valid docker-compose.json`, async () => { + const fileContent = await getFile(app, 'docker-compose.json') + const parsed = dynamicComposeSchema.safeParse(JSON.parse(fileContent || '{}')) + + if (!parsed.success) { + const validationError = fromError(parsed.error); + console.error(`Error parsing docker-compose.json for app ${app}:`, validationError.toString()); + } + + expect(parsed.success).toBe(true) + }) + } +}); diff --git a/apps/whoami/config.json b/apps/whoami/config.json new file mode 100644 index 0000000..c0bee99 --- /dev/null +++ b/apps/whoami/config.json @@ -0,0 +1,24 @@ +{ + "name": "Whoami", + "id": "whoami", + "available": true, + "short_desc": "Tiny Go server that prints os information and HTTP request to output.", + "author": "traefik", + "port": 8382, + "categories": [ + "utilities" + ], + "description": "Tiny Go webserver that prints OS information and HTTP request to output.", + "tipi_version": 2, + "version": "v1.11.0", + "source": "https://github.com/traefik/whoami", + "exposable": true, + "supported_architectures": [ + "arm64", + "amd64" + ], + "created_at": 1745082405284, + "updated_at": 1745674974072, + "dynamic_config": true, + "form_fields": [] +} \ No newline at end of file diff --git a/apps/whoami/docker-compose.json b/apps/whoami/docker-compose.json new file mode 100644 index 0000000..2f6c4a1 --- /dev/null +++ b/apps/whoami/docker-compose.json @@ -0,0 +1,10 @@ +{ + "services": [ + { + "name": "whoami", + "image": "traefik/whoami:v1.11.0", + "isMain": true, + "internalPort": "80" + } + ] +} diff --git a/apps/whoami/metadata/description.md b/apps/whoami/metadata/description.md new file mode 100644 index 0000000..27cd734 --- /dev/null +++ b/apps/whoami/metadata/description.md @@ -0,0 +1,43 @@ +# Whoami + +Tiny Go webserver that prints OS information and HTTP request to output. + +## Usage + +### Paths + +#### `/[?wait=d]` + +Returns the whoami information (request and network information). + +The optional `wait` query parameter can be provided to tell the server to wait before sending the response. +The duration is expected in Go's [`time.Duration`](https://golang.org/pkg/time/#ParseDuration) format (e.g. `/?wait=100ms` to wait 100 milliseconds). + +The optional `env` query parameter can be set to `true` to add the environment variables to the response. + +#### `/api` + +Returns the whoami information (and some extra information) as JSON. + +The optional `env` query parameter can be set to `true` to add the environment variables to the response. + +#### `/bench` + +Always return the same response (`1`). + +#### `/data?size=n[&unit=u]` + +Creates a response with a size `n`. + +The unit of measure, if specified, accepts the following values: `KB`, `MB`, `GB`, `TB` (optional, default: bytes). + +#### `/echo` + +WebSocket echo. + +#### `/health` + +Heath check. + +- `GET`, `HEAD`, ...: returns a response with the status code defined by the `POST` +- `POST`: changes the status code of the `GET` (`HEAD`, ...) response. diff --git a/apps/whoami/metadata/logo.jpg b/apps/whoami/metadata/logo.jpg new file mode 100644 index 0000000..24ed99d Binary files /dev/null and b/apps/whoami/metadata/logo.jpg differ diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..a285984 Binary files /dev/null and b/bun.lockb differ diff --git a/config.js b/config.js new file mode 100644 index 0000000..9a95d69 --- /dev/null +++ b/config.js @@ -0,0 +1,3 @@ +export default { + allowedCommands: ["bun ./scripts/update-config.ts", "bun install && bun run test"], +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..925fe34 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "example-appstore", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "test": "bun test" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^22.14.1" + }, + "dependencies": { + "@runtipi/common": "^0.8.0", + "bun": "^1.2.10", + "zod-validation-error": "^3.4.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..936fd91 --- /dev/null +++ b/renovate.json @@ -0,0 +1,62 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "automerge": false, + "extends": [ + "config:recommended" + ], + "addLabels": [ + "renovate" + ], + "enabledManagers": ["regex"], + "automergeStrategy": "rebase", + "customManagers": [ + { + "customType": "regex", + "fileMatch": [ + "^.*docker-compose\\.json$" + ], + "matchStrings": [ + "\"image\": \"(?.*?):(?.*?)\"," + ], + "datasourceTemplate": "docker" + } + ], + "packageRules": [ + { + "matchUpdateTypes": [ + "minor", + "major", + "patch", + "pin", + "digest" + ], + "automerge": false + }, + { + "matchDepTypes": [ + "devDependencies" + ], + "automerge": false + }, + { + "matchPackageNames": [ + "mariadb", + "mysql", + "monogdb", + "postgres", + "redis" + ], + "enabled": false + } + ], + "postUpgradeTasks": { + "commands": [ + "bun ./scripts/update-config.ts {{packageFile}} {{newVersion}}", + "bun install && bun run test" + ], + "fileFilters": [ + "**/*" + ], + "executionMode": "update" + } +} diff --git a/scripts/update-config.ts b/scripts/update-config.ts new file mode 100644 index 0000000..42891c7 --- /dev/null +++ b/scripts/update-config.ts @@ -0,0 +1,35 @@ +import path from "node:path"; +import fs from "fs/promises"; + +const packageFile = process.argv[2]; +const newVersion = process.argv[3]; + +type AppConfig = { + tipi_version: string; + version: string; + updated_at: number; +}; + +const updateAppConfig = async (packageFile: string, newVersion: string) => { + try { + const packageRoot = path.dirname(packageFile); + const configPath = path.join(packageRoot, "config.json"); + + const config = await fs.readFile(configPath, "utf-8"); + const configParsed = JSON.parse(config) as AppConfig; + + configParsed.tipi_version = configParsed.tipi_version + 1; + configParsed.version = newVersion; + configParsed.updated_at = new Date().getTime(); + + await fs.writeFile(configPath, JSON.stringify(configParsed, null, 2)); + } catch (e) { + console.error(`Failed to update app config, error: ${e}`); + } +}; + +if (!packageFile || !newVersion) { + console.error("Usage: node update-config.js "); + process.exit(1); +} +updateAppConfig(packageFile, newVersion); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..01ef57a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "skipLibCheck": true, + "target": "es2022", + "allowJs": true, + "resolveJsonModule": true, + "moduleDetection": "force", + "isolatedModules": true, + "verbatimModuleSyntax": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "module": "NodeNext", + "outDir": "dist", + "sourceMap": true, + "lib": [ + "es2022" + ] + }, + "include": [ + "**/*.ts", + ], +}