mirror of
https://github.com/hhftechnology/Marketplace.git
synced 2026-03-08 23:04:19 -05:00
update-new-templates
This commit is contained in:
26
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
26
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -1,5 +1,5 @@
|
||||
name: Template Bug Report
|
||||
description: Report a bug in one of our open source templates (Adguard, Appwrite, etc.)
|
||||
description: Report a bug in one of our open source templates (Supabase, Appwrite, etc.)
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
@@ -7,24 +7,13 @@ body:
|
||||
value: |
|
||||
Before opening a new issue, please search existing issues to see if this bug has already been reported.
|
||||
|
||||
This template is specifically for bugs in our open source templates like Adguard, Appwrite, and others.
|
||||
This template is specifically for bugs in our open source templates like Supabase, Appwrite, and others.
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Service/Template Name
|
||||
description: Which service or template is experiencing the bug?
|
||||
placeholder: "e.g: nginx, redis, adguard, etc."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Structure Type
|
||||
description: Which structure format is this service using?
|
||||
options:
|
||||
- "Flat file (compose-files/service-name.yml)"
|
||||
- "Folder structure (compose-files/services/service-name/)"
|
||||
- "Not sure"
|
||||
label: Template Name
|
||||
description: Which template is experiencing the bug?
|
||||
placeholder: "e.g: Supabase, Appwrite, Plausible, etc."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -32,7 +21,7 @@ body:
|
||||
attributes:
|
||||
label: Relevant Logs of the Error
|
||||
description: |
|
||||
**IMPORTANT:** Go to the logs tab in your Compose Logs and take a screenshot of the error.
|
||||
**IMPORTANT:** Please provide clear error logs from your deployment environment.
|
||||
Please be clear and include the full error message. You can also paste text logs here.
|
||||
placeholder: |
|
||||
Please attach a clear screenshot of the error logs from the logs tab.
|
||||
@@ -69,7 +58,8 @@ body:
|
||||
render: bash
|
||||
placeholder: |
|
||||
Operating System: Ubuntu 20.04
|
||||
Compose version: 2.40.3
|
||||
Docker Version: [e.g., 24.0.0]
|
||||
Docker Compose Version: [e.g., 2.20.0]
|
||||
VPS Provider: DigitalOcean, Hetzner, etc.
|
||||
Template Version: [if known]
|
||||
Browser: Chrome, Firefox, etc. [if relevant]
|
||||
|
||||
11
.github/ISSUE_TEMPLATE/config.yml
vendored
11
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,11 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: New Feature request
|
||||
url: https://github.com/hhftechnology/Dock-Dploy/discussions/new?category=request-feature
|
||||
about: For feature/script requests, please use the Discussions section.
|
||||
- name: 🤔 Questions and Help
|
||||
url: https://forum.hhf.technology/c/help/85
|
||||
about: For suggestions or questions, please use the Our forums.
|
||||
- name: Discord
|
||||
url: https://discord.gg/HDCt9MjyMJ
|
||||
about: Join our Discord server to chat with other users in the hhf community.
|
||||
128
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
128
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -2,136 +2,48 @@ name: New Template Request
|
||||
description: Suggest a new template for the project
|
||||
labels: ["template"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Before submitting, please review the [README.md](https://github.com/hhftechnology/Marketplace/blob/main/README.md) to understand the repository structure and submission guidelines.
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Service Name
|
||||
description: Provide a clear and descriptive name for the service/template
|
||||
placeholder: "e.g: nginx, redis, postgres, etc."
|
||||
label: Template Name
|
||||
description: Provide a clear and descriptive name for the template
|
||||
placeholder: "e.g: Supabase, Appwrite, etc."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
- type: input
|
||||
attributes:
|
||||
label: Structure Preference
|
||||
description: Choose the structure format for this service
|
||||
options:
|
||||
- "Flat file (compose-files/service-name.yml)"
|
||||
- "Folder structure (compose-files/services/service-name/)"
|
||||
- "No preference"
|
||||
label: Template URL
|
||||
description: Link to the repository or resource where the template is located
|
||||
placeholder: "https://github.com/username/repo"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
- type: input
|
||||
attributes:
|
||||
label: Docker Compose Configuration
|
||||
description: |
|
||||
Paste your complete `docker-compose.yml` configuration here.
|
||||
|
||||
**Guidelines:**
|
||||
- Include inline comments (ScaleTail style) for clarity
|
||||
- Use `${VARIABLE_NAME}` syntax for configurable values
|
||||
- Use descriptive volume placeholders (e.g., `/WORK_DIR` instead of absolute paths)
|
||||
- Set timezone to `Etc/UTC` by default
|
||||
placeholder: |
|
||||
```yaml
|
||||
services:
|
||||
service-name:
|
||||
image: service:latest
|
||||
container_name: service-name
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${PORT:-8080}:8080" # Service port
|
||||
environment:
|
||||
- SERVICE_VAR=${SERVICE_VAR}
|
||||
```
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Template Metadata (Optional - Folder Structure Only)
|
||||
description: |
|
||||
If using folder structure and the service needs template variables, domain configuration, or custom mounts, provide the `template.toml` content here.
|
||||
|
||||
See examples in `compose-files/services/nginx/template.toml` or `compose-files/services/redis/template.toml`
|
||||
placeholder: |
|
||||
```toml
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
service_port = "80"
|
||||
|
||||
[config]
|
||||
[[config.domains]]
|
||||
serviceName = "service-name"
|
||||
port = 80
|
||||
host = "${main_domain}"
|
||||
path = "/"
|
||||
```
|
||||
label: Docker Compose Link
|
||||
description: Link to docker-compose.yml file or Docker documentation (optional) but would be helpful
|
||||
placeholder: "https://github.com/username/repo/blob/main/docker-compose.yml"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment Variables Example (Optional - Folder Structure Only)
|
||||
description: |
|
||||
If using folder structure and the service has environment variables, provide the `.env.example` content here.
|
||||
|
||||
See examples in the service folders for the format.
|
||||
label: Resources
|
||||
description: List the resources, links, or any other information that would be helpful to know about the template
|
||||
placeholder: |
|
||||
```bash
|
||||
# Service Configuration
|
||||
SERVICE=service-name
|
||||
IMAGE_URL=service:latest
|
||||
SERVICEPORT=8080
|
||||
|
||||
# Template Variables
|
||||
MAIN_DOMAIN=example.com
|
||||
```
|
||||
- Link to the template
|
||||
- Link to the documentation
|
||||
- Link to the repository
|
||||
- Link to the website
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Service Documentation (Optional - Folder Structure Only)
|
||||
description: |
|
||||
If using folder structure, provide a README.md content with setup instructions, features, and usage notes.
|
||||
label: Template Description
|
||||
description: Provide a detailed description of what this template does and when to use it
|
||||
placeholder: |
|
||||
# Service Name
|
||||
|
||||
Brief description of the service.
|
||||
|
||||
## Features
|
||||
- Feature 1
|
||||
- Feature 2
|
||||
|
||||
## Configuration
|
||||
Setup instructions...
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Resources and References
|
||||
description: List the resources, links, or any other information that would be helpful
|
||||
placeholder: |
|
||||
- Link to official documentation
|
||||
- Link to Docker Hub page
|
||||
- Link to GitHub repository
|
||||
- Link to website
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Service Description
|
||||
description: Provide a detailed description of what this service does and when to use it
|
||||
placeholder: |
|
||||
This service is perfect for [use case]...
|
||||
This template is perfect for creating modern web applications with React and TypeScript...
|
||||
validations:
|
||||
required: false
|
||||
|
||||
|
||||
@@ -1,63 +1,16 @@
|
||||
## What is this PR about?
|
||||
|
||||
<!-- Describe what this PR adds or changes -->
|
||||
- [ ] New service/template
|
||||
- [ ] Update to existing service
|
||||
- [ ] Bug fix
|
||||
- [ ] Documentation update
|
||||
- [ ] Other (please describe)
|
||||
|
||||
**Service Name:** [e.g., nginx, redis, postgres]
|
||||
|
||||
**Structure Type:**
|
||||
- [ ] Flat file (`compose-files/service-name.yml`)
|
||||
- [ ] Folder structure (`compose-files/services/service-name/`)
|
||||
New PR of [Template Name]
|
||||
|
||||
## Checklist
|
||||
|
||||
Before submitting this PR, please make sure that:
|
||||
|
||||
### General Requirements
|
||||
- [ ] I have read the [README.md](https://github.com/hhftechnology/Marketplace/blob/main/README.md) and followed the submission guidelines
|
||||
- [ ] I have tested the template/service in my instance
|
||||
- [ ] The Docker image(s) are publicly accessible (no private registries)
|
||||
- [ ] I have used `${VARIABLE_NAME}` syntax for configurable values
|
||||
- [ ] I have used descriptive volume placeholders (e.g., `/WORK_DIR` instead of absolute paths)
|
||||
- [ ] Timezone is set to `Etc/UTC` (if applicable)
|
||||
- [ ] I have read the suggestions in the README.md file https://github.com/hhftechnology/Marketplace?tab=readme-ov-file#general-suggestions-when-creating-a-template
|
||||
- [ ] I have tested the template in my instance, so the maintainers don't spend time trying to figure out what's wrong.
|
||||
- [ ] I have added tests that demonstrate that my correction works or that my new feature works.
|
||||
|
||||
### Docker Compose File
|
||||
- [ ] Includes inline comments (ScaleTail style) for clarity
|
||||
- [ ] Minimized unnecessary ports, environment variables, or configurations
|
||||
- [ ] Uses `latest` tag where possible (or specific version if required)
|
||||
- [ ] Multi-service support is properly configured (if applicable)
|
||||
|
||||
### Folder Structure (If Applicable)
|
||||
- [ ] `docker-compose.yml` is included and properly formatted
|
||||
- [ ] `template.toml` is included (if template variables/domain config needed)
|
||||
- [ ] `.env.example` is included (if environment variables are needed)
|
||||
- [ ] `README.md` is included (if detailed setup instructions are needed)
|
||||
|
||||
### Testing
|
||||
- [ ] I have tested the service deployment
|
||||
- [ ] I have verified all environment variables work correctly
|
||||
- [ ] I have checked that volumes and ports are correctly configured
|
||||
- [ ] I have verified the service starts and runs without errors
|
||||
|
||||
## Files Changed
|
||||
|
||||
<!-- List the files you've added or modified -->
|
||||
- `compose-files/[service-name].yml` (flat file structure)
|
||||
- OR
|
||||
- `compose-files/services/[service-name]/docker-compose.yml`
|
||||
- `compose-files/services/[service-name]/template.toml` (if applicable)
|
||||
- `compose-files/services/[service-name]/.env.example` (if applicable)
|
||||
- `compose-files/services/[service-name]/README.md` (if applicable)
|
||||
|
||||
## Additional Notes
|
||||
|
||||
<!-- Any additional context, screenshots, or information that would help reviewers -->
|
||||
|
||||
## Issues Related (if applicable)
|
||||
## Issues related (if applicable)
|
||||
|
||||
Close automatically the related issues using the keywords: `closes #ISSUE_NUMBER`, `fixes #ISSUE_NUMBER`, `resolves #ISSUE_NUMBER`
|
||||
|
||||
|
||||
64
.github/copilot-instructions.md
vendored
Normal file
64
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
# Marketplace Repository - Copilot Instructions
|
||||
|
||||
## Project Overview
|
||||
|
||||
This repository maintains Docker Compose templates for deploying open-source applications. The core structure revolves around the `compose-files/` directory, where each subdirectory represents a deployable service (e.g., `compose-files/nginx/` for Nginx web server).
|
||||
|
||||
Key components:
|
||||
|
||||
- **Compose Files**: Self-contained templates with `docker-compose.yml` (service definitions).
|
||||
- **meta.json**: Centralized index of all templates. Entries include `id`, `name`, `version`, `description`, `logo`, `links`, and `tags`.
|
||||
- **Scripts**: Node.js tools in root and `build-scripts/` for maintaining `meta.json` (deduplication, sorting, validation).
|
||||
|
||||
Data flow: New templates added to `compose-files/` → Metadata updated in `meta.json` → Processing scripts ensure consistency → Templates ready for deployment.
|
||||
|
||||
The "why": Enables rapid, standardized deployment of 300+ OSS apps without manual config. Structure prioritizes simplicity—each template is independent, no shared state or complex interdependencies.
|
||||
|
||||
## Key Files and Directories
|
||||
|
||||
- `meta.json`: Array of template objects. Always process after edits using `node dedupe-and-sort-meta.js` to remove duplicates (by `id`) and sort alphabetically.
|
||||
- `compose-files/<service>/`:
|
||||
- `docker-compose.yml`: Standard Docker Compose v3.8. May include ports, volumes, and environment variables as needed.
|
||||
- `logo.svg/png`: Service icon, referenced in `meta.json`.
|
||||
- `dedupe-and-sort-meta.js`: Standalone script—reads `meta.json`, removes duplicate `id`s (keeps first), sorts by `id` (case-insensitive), creates timestamped backup.
|
||||
- `build-scripts/process-meta.js`: Advanced processor with CLI options (`--verbose`, `--no-backup`, `--input`/`--output`), JSON schema validation (required: `id`, `name`, `version`, `description`, `links.github`, `logo`, `tags` array).
|
||||
|
||||
Exemplary template: `compose-files/nginx/`—`docker-compose.yml` defines Nginx service; meta entry tags as ["proxy", "web-server"].
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Add/Update Template**:
|
||||
|
||||
- Create `compose-files/<id>/` (e.g., `nginx`).
|
||||
- Implement `docker-compose.yml` (single or multiple services; use volumes for persistence).
|
||||
- Add/update `meta.json` entry with exact `id` matching folder.
|
||||
- Run `node dedupe-and-sort-meta.js --backup` to validate/sort.
|
||||
- Commit and push changes.
|
||||
|
||||
2. **Local Development**:
|
||||
|
||||
- Meta processing: `node build-scripts/process-meta.js --verbose` or `node dedupe-and-sort-meta.js --backup`.
|
||||
- Test template: Use `docker-compose up` in the service directory to test locally.
|
||||
|
||||
3. **CI/CD**:
|
||||
- `.github/workflows/validate-meta.yml`: Runs validation on push/PR—fails on duplicates, invalid JSON, missing fields.
|
||||
- Integrate processing: Add `node build-scripts/process-meta.js` to build steps; use `--no-backup` in CI.
|
||||
|
||||
No tests in repo—focus on manual validation via scripts and Docker Compose testing. Debug: Check console output from processing scripts for warnings (e.g., missing `id`).
|
||||
|
||||
## Conventions and Patterns
|
||||
|
||||
- **Template IDs**: Lowercase, kebab-case (e.g., `active-pieces`); unique across repo—enforced by dedupe script.
|
||||
- **Docker Compose**: Standard Docker Compose v3.8 format. Include `restart: unless-stopped`, persistent volumes (e.g., `- db-data:/var/lib/postgresql/data`). Services typically named after folder (e.g., `nginx` service).
|
||||
- **Meta.json**: Entries as JSON objects; tags array of lowercase strings (e.g., ["monitoring", "database"]); links object with `github`, `website`, `docs`.
|
||||
- **Versions**: Use `latest` tag or pin to specific versions in `docker-compose.yml` (e.g., `nginx:1.25-alpine`); match in `meta.json.version`.
|
||||
- **Logos**: SVG preferred; size ~128x128; file name in `meta.json.logo` (e.g., "nginx.svg").
|
||||
|
||||
Cross-component: No runtime communication—templates independent. Each template can be deployed standalone using Docker Compose.
|
||||
|
||||
## Integration Points
|
||||
|
||||
- **Docker Compose**: Templates can be deployed directly using `docker-compose up`. Test deploys validate env interpolation and service configuration.
|
||||
- **External Deps**: Docker Compose (v3.8+). No runtime deps beyond Node.js for meta processing scripts.
|
||||
|
||||
When editing, always re-run meta processing and validate template deployment.
|
||||
80
.github/workflows/validate-meta.yml
vendored
Normal file
80
.github/workflows/validate-meta.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
name: Validate and Process Meta.json
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master, develop]
|
||||
paths: ["meta.json"]
|
||||
pull_request:
|
||||
branches: [main, master]
|
||||
paths: ["meta.json"]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
validate-meta:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Validate meta.json structure
|
||||
run: |
|
||||
echo "🔍 Validating meta.json structure..."
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const data = JSON.parse(fs.readFileSync('meta.json', 'utf8'));
|
||||
if (!Array.isArray(data)) throw new Error('meta.json must be an array');
|
||||
console.log('✅ meta.json structure is valid');
|
||||
console.log('📊 Found', data.length, 'entries');
|
||||
"
|
||||
|
||||
- name: Check for duplicates and sort order
|
||||
run: |
|
||||
echo "🔍 Checking for duplicates and sort order..."
|
||||
node build-scripts/process-meta.js --verbose --output /tmp/meta-test.json
|
||||
|
||||
- name: Compare with original
|
||||
run: |
|
||||
echo "🔍 Comparing processed file with original..."
|
||||
if ! diff -q meta.json /tmp/meta-test.json > /dev/null; then
|
||||
echo "⚠️ meta.json needs processing (duplicates found or not sorted)"
|
||||
echo "Original entries:"
|
||||
node -e "console.log(JSON.parse(require('fs').readFileSync('meta.json', 'utf8')).length)"
|
||||
echo "Processed entries:"
|
||||
node -e "console.log(JSON.parse(require('fs').readFileSync('/tmp/meta-test.json', 'utf8')).length)"
|
||||
echo ""
|
||||
echo "To fix this, run: npm run process-meta"
|
||||
exit 1
|
||||
else
|
||||
echo "✅ meta.json is properly deduplicated and sorted"
|
||||
fi
|
||||
|
||||
- name: Validate required fields
|
||||
run: |
|
||||
echo "🔍 Validating required fields..."
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const data = JSON.parse(fs.readFileSync('meta.json', 'utf8'));
|
||||
const required = ['id', 'name', 'version', 'description', 'links', 'logo', 'tags'];
|
||||
let issues = 0;
|
||||
|
||||
data.forEach((item, index) => {
|
||||
const missing = required.filter(field => !item[field]);
|
||||
if (missing.length > 0) {
|
||||
console.log('❌ Entry', index, '(' + item.id + '):', 'Missing fields:', missing.join(', '));
|
||||
issues++;
|
||||
}
|
||||
});
|
||||
|
||||
if (issues > 0) {
|
||||
console.log('🚨 Found', issues, 'entries with missing required fields');
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('✅ All entries have required fields');
|
||||
}
|
||||
"
|
||||
181
.github/workflows/validate.yml
vendored
Normal file
181
.github/workflows/validate.yml
vendored
Normal file
@@ -0,0 +1,181 @@
|
||||
name: Validate Compose Files Structure and Meta
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Validate compose-files folders and files
|
||||
run: |
|
||||
echo "🔍 Validating compose-files folder structure..."
|
||||
|
||||
ERROR=0
|
||||
|
||||
# Loop through each service folder
|
||||
for dir in compose-files/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
TEMPLATE_NAME=$(basename "$dir")
|
||||
|
||||
COMPOSE_FILE="$dir/docker-compose.yml"
|
||||
|
||||
if [ ! -f "$COMPOSE_FILE" ]; then
|
||||
echo "❌ Missing docker-compose.yml in $TEMPLATE_NAME"
|
||||
ERROR=1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $ERROR -eq 1 ]; then
|
||||
echo "❌ Compose files folder validation failed."
|
||||
exit 1
|
||||
else
|
||||
echo "✅ Compose files folders validated successfully."
|
||||
fi
|
||||
|
||||
- name: Validate meta.json structure and required fields
|
||||
run: |
|
||||
echo "🔍 Validating meta.json structure and required fields..."
|
||||
|
||||
# First check if meta.json exists and is valid JSON
|
||||
if [ ! -f "meta.json" ]; then
|
||||
echo "❌ meta.json file not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! jq empty meta.json 2>/dev/null; then
|
||||
echo "❌ meta.json is not a valid JSON file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ERROR=0
|
||||
ERRORS_FOUND=""
|
||||
|
||||
# Debug: Show total number of entries
|
||||
TOTAL_ENTRIES=$(jq '. | length' meta.json)
|
||||
echo "📊 Total entries in meta.json: $TOTAL_ENTRIES"
|
||||
|
||||
# Get all entries at once and process them
|
||||
TOTAL_INDEX=$(($TOTAL_ENTRIES - 1))
|
||||
|
||||
for i in $(seq 0 $TOTAL_INDEX); do
|
||||
entry=$(jq -c ".[$i]" meta.json)
|
||||
INDEX=$((i + 1))
|
||||
|
||||
echo "-------------------------------------------"
|
||||
echo "🔍 Checking entry #$INDEX..."
|
||||
|
||||
# Get the ID for better error reporting
|
||||
ID=$(echo "$entry" | jq -r '.id // "UNKNOWN"')
|
||||
echo "📝 Processing entry with ID: $ID"
|
||||
|
||||
# Validate required top-level fields
|
||||
for field in "id" "name" "version" "description" "logo" "links" "tags"; do
|
||||
if [ "$(echo "$entry" | jq "has(\"$field\")")" != "true" ]; then
|
||||
ERROR_MSG="❌ Entry #$INDEX (ID: $ID) is missing required field: $field"
|
||||
echo "$ERROR_MSG"
|
||||
ERRORS_FOUND="${ERRORS_FOUND}${ERROR_MSG}\n"
|
||||
ERROR=1
|
||||
fi
|
||||
done
|
||||
|
||||
# Validate links object required fields
|
||||
if [ "$(echo "$entry" | jq 'has("links")')" == "true" ]; then
|
||||
for link_field in "github" "website" "docs"; do
|
||||
if [ "$(echo "$entry" | jq ".links | has(\"$link_field\")")" != "true" ]; then
|
||||
ERROR_MSG="❌ Entry #$INDEX (ID: $ID): links object is missing required field: $link_field"
|
||||
echo "$ERROR_MSG"
|
||||
ERRORS_FOUND="${ERRORS_FOUND}${ERROR_MSG}\n"
|
||||
ERROR=1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Validate tags array is not empty
|
||||
if [ "$(echo "$entry" | jq '.tags | length')" -eq 0 ]; then
|
||||
ERROR_MSG="❌ Entry #$INDEX (ID: $ID): tags array cannot be empty"
|
||||
echo "$ERROR_MSG"
|
||||
ERRORS_FOUND="${ERRORS_FOUND}${ERROR_MSG}\n"
|
||||
ERROR=1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "-------------------------------------------"
|
||||
if [ $ERROR -eq 1 ]; then
|
||||
echo "❌ meta.json structure validation failed."
|
||||
echo -e "Summary of all errors found:${ERRORS_FOUND}"
|
||||
exit 1
|
||||
else
|
||||
echo "✅ meta.json structure validated successfully."
|
||||
fi
|
||||
|
||||
- name: Validate meta.json matches compose-files folders and logo files
|
||||
run: |
|
||||
echo "🔍 Validating meta.json against compose-files folders and logos..."
|
||||
|
||||
ERROR=0
|
||||
|
||||
# Read all compose-files folder names into an array
|
||||
FOLDERS=($(ls -1 compose-files))
|
||||
|
||||
# Extract ids and logos from meta.json
|
||||
IDS_AND_LOGOS=$(jq -c '.[] | {id, logo}' meta.json)
|
||||
|
||||
# Validate each id in meta.json exists as a folder
|
||||
for item in $IDS_AND_LOGOS; do
|
||||
ID=$(echo "$item" | jq -r '.id')
|
||||
LOGO=$(echo "$item" | jq -r '.logo')
|
||||
|
||||
# Check if folder exists
|
||||
if [ ! -d "compose-files/$ID" ]; then
|
||||
echo "❌ meta.json id \"$ID\" does not have a matching folder in compose-files/"
|
||||
ERROR=1
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check if logo file exists inside its folder (if logo is specified)
|
||||
if [ "$LOGO" != "null" ] && [ -n "$LOGO" ]; then
|
||||
if [ ! -f "compose-files/$ID/$LOGO" ]; then
|
||||
echo "⚠️ Logo \"$LOGO\" defined for \"$ID\" does not exist in compose-files/$ID/ (warning only)"
|
||||
# This is a warning, not an error, as logos are optional
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Validate each folder has a matching id in meta.json
|
||||
META_IDS=$(jq -r '.[].id' meta.json)
|
||||
for FOLDER in "${FOLDERS[@]}"; do
|
||||
# Skip the services folder if it exists
|
||||
if [ "$FOLDER" == "services" ]; then
|
||||
continue
|
||||
fi
|
||||
FOUND=0
|
||||
for ID in $META_IDS; do
|
||||
if [ "$FOLDER" == "$ID" ]; then
|
||||
FOUND=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$FOUND" -eq 0 ]; then
|
||||
echo "❌ Folder \"$FOLDER\" has no matching id in meta.json"
|
||||
ERROR=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $ERROR -eq 1 ]; then
|
||||
echo "❌ meta.json validation failed."
|
||||
exit 1
|
||||
else
|
||||
echo "✅ meta.json validated successfully."
|
||||
fi
|
||||
22
README.md
22
README.md
@@ -13,20 +13,17 @@ compose-files/
|
||||
└── services/
|
||||
├── nginx/
|
||||
│ ├── docker-compose.yml # Main compose file
|
||||
│ ├── template.toml # Template metadata (optional)
|
||||
│ ├── .env.example # Example env file (optional)
|
||||
│ └── README.md # Service docs (optional)
|
||||
│
|
||||
└── redis/
|
||||
├── docker-compose.yml
|
||||
├── template.toml
|
||||
└── .env.example
|
||||
```
|
||||
|
||||
### File Formats
|
||||
|
||||
- **docker-compose.yml**: Standard Docker Compose file with comments (ScaleTail style)
|
||||
- **template.toml**: Dokploy-style template metadata for template variables and domain configuration (optional)
|
||||
- **.env.example**: ScaleTail-style environment file with comments showing required variables (optional)
|
||||
- **README.md**: Service-specific documentation (optional)
|
||||
|
||||
@@ -62,24 +59,6 @@ Use this structure in your issue description:
|
||||
|
||||
If using folder structure, you may include:
|
||||
|
||||
**template.toml** (if template variables are needed):
|
||||
|
||||
```toml
|
||||
[variables]
|
||||
main_domain = "${domain}"
|
||||
service_port = "80"
|
||||
|
||||
[config]
|
||||
[[config.domains]]
|
||||
serviceName = "nginx"
|
||||
port = 80
|
||||
host = "${main_domain}"
|
||||
path = "/"
|
||||
|
||||
[config.env]
|
||||
NGINX_HOST = "${main_domain}"
|
||||
```
|
||||
|
||||
**.env.example** (if environment variables are needed):
|
||||
|
||||
```bash
|
||||
@@ -121,7 +100,6 @@ Before opening your issue, please verify that your submission meets these standa
|
||||
### Folder Structure Requirements (If Using)
|
||||
|
||||
- **docker-compose.yml**: Required - Main compose file
|
||||
- **template.toml**: Optional - Include if service needs template variables, domain configuration, or custom mounts
|
||||
- **.env.example**: Optional - Include if service has environment variables that users need to configure
|
||||
- **README.md**: Optional - Include for services that need detailed setup instructions or have special requirements
|
||||
|
||||
|
||||
292
build-scripts/process-meta.js
Normal file
292
build-scripts/process-meta.js
Normal file
@@ -0,0 +1,292 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Production build script for processing meta.json
|
||||
* This script is designed to be run during CI/CD or build processes
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
class MetaProcessor {
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
inputFile: options.inputFile || "meta.json",
|
||||
outputFile: options.outputFile || null, // If null, overwrites input
|
||||
createBackup: options.createBackup || false, // Default false
|
||||
verbose: options.verbose || false,
|
||||
validateSchema: options.validateSchema !== false, // Default true
|
||||
exitOnError: options.exitOnError !== false, // Default true
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
log(message, level = "info") {
|
||||
if (!this.options.verbose && level === "debug") return;
|
||||
|
||||
const timestamp = new Date().toISOString();
|
||||
const prefix =
|
||||
{
|
||||
info: "🔧",
|
||||
success: "✅",
|
||||
warning: "⚠️",
|
||||
error: "❌",
|
||||
debug: "🔍",
|
||||
}[level] || "ℹ️";
|
||||
|
||||
console.log(`[${timestamp}] ${prefix} ${message}`);
|
||||
}
|
||||
|
||||
validateSchema(item, index) {
|
||||
const requiredFields = [
|
||||
"id",
|
||||
"name",
|
||||
"version",
|
||||
"description",
|
||||
"links",
|
||||
"logo",
|
||||
"tags",
|
||||
];
|
||||
const missing = requiredFields.filter((field) => !item[field]);
|
||||
|
||||
if (missing.length > 0) {
|
||||
this.log(
|
||||
`Item at index ${index} missing required fields: ${missing.join(", ")}`,
|
||||
"warning"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate links structure
|
||||
if (typeof item.links !== "object" || !item.links.github) {
|
||||
this.log(`Item "${item.id}" has invalid links structure`, "warning");
|
||||
}
|
||||
|
||||
// Validate tags is array
|
||||
if (!Array.isArray(item.tags)) {
|
||||
this.log(
|
||||
`Item "${item.id}" has invalid tags (should be array)`,
|
||||
"warning"
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async process() {
|
||||
const startTime = Date.now();
|
||||
this.log(`Starting meta.json processing...`);
|
||||
|
||||
try {
|
||||
// Read input file
|
||||
if (!fs.existsSync(this.options.inputFile)) {
|
||||
throw new Error(`Input file not found: ${this.options.inputFile}`);
|
||||
}
|
||||
|
||||
const fileContent = fs.readFileSync(this.options.inputFile, "utf8");
|
||||
let data;
|
||||
|
||||
try {
|
||||
data = JSON.parse(fileContent);
|
||||
} catch (parseError) {
|
||||
throw new Error(
|
||||
`Invalid JSON in ${this.options.inputFile}: ${parseError.message}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error(
|
||||
`Expected array in ${this.options.inputFile}, got ${typeof data}`
|
||||
);
|
||||
}
|
||||
|
||||
this.log(`Found ${data.length} total entries`);
|
||||
|
||||
// Process data
|
||||
const results = this.dedupeAndSort(data);
|
||||
|
||||
// Create backup if requested
|
||||
if (this.options.createBackup) {
|
||||
const backupPath = `${this.options.inputFile}.backup.${Date.now()}`;
|
||||
fs.writeFileSync(backupPath, fileContent, "utf8");
|
||||
this.log(`Backup created: ${backupPath}`, "debug");
|
||||
}
|
||||
|
||||
// Write output
|
||||
const outputFile = this.options.outputFile || this.options.inputFile;
|
||||
const newContent = this.formatJSON(results.unique) + "\n";
|
||||
fs.writeFileSync(outputFile, newContent, "utf8");
|
||||
|
||||
// Report results
|
||||
const duration = Date.now() - startTime;
|
||||
this.log(`Processing completed in ${duration}ms`, "success");
|
||||
this.log(`Statistics:`, "info");
|
||||
this.log(` • Original entries: ${results.original}`, "info");
|
||||
this.log(` • Duplicates removed: ${results.duplicatesRemoved}`, "info");
|
||||
this.log(` • Final entries: ${results.final}`, "info");
|
||||
this.log(` • Schema violations: ${results.schemaViolations}`, "info");
|
||||
|
||||
if (results.duplicates.length > 0) {
|
||||
this.log(`Removed duplicates:`, "warning");
|
||||
results.duplicates.forEach((dup) => {
|
||||
this.log(` • "${dup.id}" (${dup.name})`, "warning");
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
this.log(`Processing failed: ${error.message}`, "error");
|
||||
if (this.options.exitOnError) {
|
||||
process.exit(1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
dedupeAndSort(data) {
|
||||
const seenIds = new Set();
|
||||
const duplicates = [];
|
||||
const unique = [];
|
||||
let schemaViolations = 0;
|
||||
|
||||
data.forEach((item, index) => {
|
||||
if (!item || typeof item !== "object") {
|
||||
this.log(`Skipping invalid item at index ${index}`, "warning");
|
||||
schemaViolations++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.id) {
|
||||
this.log(
|
||||
`Skipping item without ID at index ${index}: ${
|
||||
item.name || "Unknown"
|
||||
}`,
|
||||
"warning"
|
||||
);
|
||||
schemaViolations++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate schema if enabled
|
||||
if (this.options.validateSchema) {
|
||||
if (!this.validateSchema(item, index)) {
|
||||
schemaViolations++;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
if (seenIds.has(item.id)) {
|
||||
duplicates.push({
|
||||
id: item.id,
|
||||
name: item.name || "Unknown",
|
||||
originalIndex: index,
|
||||
});
|
||||
this.log(
|
||||
`Duplicate ID found: "${item.id}" (${item.name || "Unknown"})`,
|
||||
"warning"
|
||||
);
|
||||
} else {
|
||||
seenIds.add(item.id);
|
||||
unique.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort alphabetically by ID (ASCII order)
|
||||
unique.sort((a, b) => {
|
||||
const idA = a.id.toLowerCase();
|
||||
const idB = b.id.toLowerCase();
|
||||
return idA < idB ? -1 : idA > idB ? 1 : 0;
|
||||
});
|
||||
|
||||
return {
|
||||
original: data.length,
|
||||
duplicatesRemoved: duplicates.length,
|
||||
final: unique.length,
|
||||
duplicates,
|
||||
unique,
|
||||
schemaViolations,
|
||||
};
|
||||
}
|
||||
|
||||
formatJSON(data) {
|
||||
// Custom JSON formatter that keeps small arrays compact
|
||||
return JSON.stringify(
|
||||
data,
|
||||
(key, value) => {
|
||||
if (Array.isArray(value)) {
|
||||
// Keep arrays compact if they're small and contain only strings
|
||||
if (
|
||||
value.length <= 5 &&
|
||||
value.every((item) => typeof item === "string" && item.length < 50)
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
2
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// CLI usage
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
const options = {};
|
||||
|
||||
// Parse command line arguments
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
switch (arg) {
|
||||
case "--input":
|
||||
case "-i":
|
||||
options.inputFile = args[++i];
|
||||
break;
|
||||
case "--output":
|
||||
case "-o":
|
||||
options.outputFile = args[++i];
|
||||
break;
|
||||
case "--backup":
|
||||
options.createBackup = true;
|
||||
break;
|
||||
case "--no-backup":
|
||||
options.createBackup = false;
|
||||
break;
|
||||
case "--verbose":
|
||||
case "-v":
|
||||
options.verbose = true;
|
||||
break;
|
||||
case "--no-schema-validation":
|
||||
options.validateSchema = false;
|
||||
break;
|
||||
case "--help":
|
||||
case "-h":
|
||||
console.log(`
|
||||
Usage: node process-meta.js [options]
|
||||
|
||||
Options:
|
||||
-i, --input <file> Input file path (default: meta.json)
|
||||
-o, --output <file> Output file path (default: same as input)
|
||||
--backup Create backup file (disabled by default)
|
||||
-v, --verbose Verbose output
|
||||
--no-schema-validation Skip schema validation
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
node process-meta.js
|
||||
node process-meta.js --input data/meta.json --output dist/meta.json
|
||||
node process-meta.js --verbose --no-backup
|
||||
`);
|
||||
process.exit(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const processor = new MetaProcessor(options);
|
||||
processor.process().catch((error) => {
|
||||
console.error("Process failed:", error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = MetaProcessor;
|
||||
0
compose-files/services-list.txt
Normal file
0
compose-files/services-list.txt
Normal file
182
dedupe-and-sort-meta.js
Normal file
182
dedupe-and-sort-meta.js
Normal file
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
/**
|
||||
* Remove duplicate IDs from meta.json and arrange them alphabetically
|
||||
* Usage: node dedupe-and-sort-meta.js [options] [meta.json path]
|
||||
* Options:
|
||||
* --backup Create backup before processing
|
||||
* --help Show help message
|
||||
*/
|
||||
|
||||
function dedupeAndSortMeta(filePath = "meta.json", options = {}) {
|
||||
console.log(`🔧 Processing ${filePath}...`);
|
||||
|
||||
try {
|
||||
// Check if file exists
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error(`File not found: ${filePath}`);
|
||||
}
|
||||
|
||||
// Read and parse the JSON file
|
||||
const fileContent = fs.readFileSync(filePath, "utf8");
|
||||
let data;
|
||||
|
||||
try {
|
||||
data = JSON.parse(fileContent);
|
||||
} catch (parseError) {
|
||||
throw new Error(`Invalid JSON in ${filePath}: ${parseError.message}`);
|
||||
}
|
||||
|
||||
// Validate that data is an array
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error(`Expected an array in ${filePath}, got ${typeof data}`);
|
||||
}
|
||||
|
||||
console.log(`📊 Found ${data.length} total entries`);
|
||||
|
||||
// Track duplicates and stats
|
||||
const seenIds = new Set();
|
||||
const duplicates = [];
|
||||
const unique = [];
|
||||
|
||||
// Remove duplicates (keep first occurrence)
|
||||
data.forEach((item, index) => {
|
||||
if (!item || typeof item !== "object") {
|
||||
console.warn(`⚠️ Skipping invalid item at index ${index}:`, item);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.id) {
|
||||
console.warn(
|
||||
`⚠️ Skipping item without ID at index ${index}:`,
|
||||
item.name || "Unknown"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (seenIds.has(item.id)) {
|
||||
duplicates.push({
|
||||
id: item.id,
|
||||
name: item.name || "Unknown",
|
||||
originalIndex: index,
|
||||
});
|
||||
console.warn(
|
||||
`🔍 Duplicate ID found: "${item.id}" (${item.name || "Unknown"})`
|
||||
);
|
||||
} else {
|
||||
seenIds.add(item.id);
|
||||
unique.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort alphabetically by ID (ASCII order)
|
||||
unique.sort((a, b) => {
|
||||
const idA = a.id.toLowerCase();
|
||||
const idB = b.id.toLowerCase();
|
||||
return idA < idB ? -1 : idA > idB ? 1 : 0;
|
||||
});
|
||||
|
||||
// Create backup if requested
|
||||
if (options.createBackup) {
|
||||
const backupPath = `${filePath}.backup.${Date.now()}`;
|
||||
fs.writeFileSync(backupPath, fileContent, "utf8");
|
||||
console.log(`💾 Backup created: ${backupPath}`);
|
||||
}
|
||||
|
||||
// Custom JSON formatter that keeps small arrays compact
|
||||
function formatJSON(data) {
|
||||
return JSON.stringify(
|
||||
data,
|
||||
(key, value) => {
|
||||
if (Array.isArray(value)) {
|
||||
// Keep arrays compact if they're small and contain only strings
|
||||
if (
|
||||
value.length <= 5 &&
|
||||
value.every(
|
||||
(item) => typeof item === "string" && item.length < 50
|
||||
)
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
// Write the cleaned and sorted data
|
||||
const newContent = formatJSON(unique) + "\n";
|
||||
fs.writeFileSync(filePath, newContent, "utf8");
|
||||
|
||||
// Report results
|
||||
console.log("\n✅ Processing completed successfully!");
|
||||
console.log(`📈 Statistics:`);
|
||||
console.log(` • Original entries: ${data.length}`);
|
||||
console.log(` • Duplicates removed: ${duplicates.length}`);
|
||||
console.log(` • Final entries: ${unique.length}`);
|
||||
console.log(` • Entries sorted alphabetically by ID`);
|
||||
|
||||
if (duplicates.length > 0) {
|
||||
console.log(`\n🗑️ Removed duplicates:`);
|
||||
duplicates.forEach((dup) => {
|
||||
console.log(` • "${dup.id}" (${dup.name})`);
|
||||
});
|
||||
}
|
||||
|
||||
// Verify the result
|
||||
const firstFew = unique.slice(0, 5).map((item) => item.id);
|
||||
const lastFew = unique.slice(-5).map((item) => item.id);
|
||||
console.log(
|
||||
`\n🔤 ID range: ${firstFew[0]} ... ${lastFew[lastFew.length - 1]}`
|
||||
);
|
||||
|
||||
return {
|
||||
original: data.length,
|
||||
duplicatesRemoved: duplicates.length,
|
||||
final: unique.length,
|
||||
duplicates: duplicates,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`❌ Error processing ${filePath}:`, error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// CLI usage
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2);
|
||||
const options = { createBackup: false };
|
||||
let filePath = "meta.json";
|
||||
|
||||
// Parse command line arguments
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg === "--backup") {
|
||||
options.createBackup = true;
|
||||
} else if (arg === "--help" || arg === "-h") {
|
||||
console.log(`
|
||||
Usage: node dedupe-and-sort-meta.js [options] [file]
|
||||
|
||||
Options:
|
||||
--backup Create backup before processing (disabled by default)
|
||||
--help Show this help message
|
||||
|
||||
Examples:
|
||||
node dedupe-and-sort-meta.js # Process meta.json without backup
|
||||
node dedupe-and-sort-meta.js --backup # Process meta.json with backup
|
||||
node dedupe-and-sort-meta.js --backup data.json # Process data.json with backup
|
||||
`);
|
||||
process.exit(0);
|
||||
} else if (!arg.startsWith("--")) {
|
||||
filePath = arg;
|
||||
}
|
||||
}
|
||||
|
||||
dedupeAndSortMeta(filePath, options);
|
||||
}
|
||||
|
||||
module.exports = dedupeAndSortMeta;
|
||||
Reference in New Issue
Block a user