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