first commit
This commit is contained in:
32
README.md
32
README.md
@@ -1,3 +1,31 @@
|
|||||||
# robin-runtipi-store
|
# Example App Store Template
|
||||||
|
|
||||||
personal runtipi store
|
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)
|
||||||
|
|||||||
77
__tests__/apps.test.ts
Normal file
77
__tests__/apps.test.ts
Normal file
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
24
apps/whoami/config.json
Normal file
24
apps/whoami/config.json
Normal file
@@ -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": []
|
||||||
|
}
|
||||||
10
apps/whoami/docker-compose.json
Normal file
10
apps/whoami/docker-compose.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"name": "whoami",
|
||||||
|
"image": "traefik/whoami:v1.11.0",
|
||||||
|
"isMain": true,
|
||||||
|
"internalPort": "80"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
43
apps/whoami/metadata/description.md
Normal file
43
apps/whoami/metadata/description.md
Normal file
@@ -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.
|
||||||
BIN
apps/whoami/metadata/logo.jpg
Normal file
BIN
apps/whoami/metadata/logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
3
config.js
Normal file
3
config.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
allowedCommands: ["bun ./scripts/update-config.ts", "bun install && bun run test"],
|
||||||
|
};
|
||||||
25
package.json
Normal file
25
package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
62
renovate.json
Normal file
62
renovate.json
Normal file
@@ -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\": \"(?<depName>.*?):(?<currentValue>.*?)\","
|
||||||
|
],
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
35
scripts/update-config.ts
Normal file
35
scripts/update-config.ts
Normal file
@@ -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 <packageFile> <newVersion>");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
updateAppConfig(packageFile, newVersion);
|
||||||
24
tsconfig.json
Normal file
24
tsconfig.json
Normal file
@@ -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",
|
||||||
|
],
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user