first commit

This commit is contained in:
2025-12-07 17:11:55 +01:00
parent d7941d358e
commit 85fd795207
12 changed files with 333 additions and 2 deletions

View File

@@ -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
View 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
View 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": []
}

View File

@@ -0,0 +1,10 @@
{
"services": [
{
"name": "whoami",
"image": "traefik/whoami:v1.11.0",
"isMain": true,
"internalPort": "80"
}
]
}

View 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
bun.lockb Executable file

Binary file not shown.

3
config.js Normal file
View File

@@ -0,0 +1,3 @@
export default {
allowedCommands: ["bun ./scripts/update-config.ts", "bun install && bun run test"],
};

25
package.json Normal file
View 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
View 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
View 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
View 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",
],
}