diff --git a/skills/README.md b/skills/README.md index 23a47a8..47f44ca 100644 --- a/skills/README.md +++ b/skills/README.md @@ -10,6 +10,12 @@ cd /root/github/1Panel-Appstore/skills # 从 compose 生成草稿,跳过图标下载 ./scripts/generate-app.sh --app-key my-app --name MyApp --version 1.2.3 --icon-mode skip ./docker-compose.yml +# 多服务 compose 指定主服务 +./scripts/generate-app.sh --service web --app-key my-app --name MyApp --version 1.2.3 --icon-mode skip ./docker-compose.yml + +# 检查依赖 +./scripts/generate-app.sh --check-deps + # 下载或复用图标 ./scripts/download-icon.sh --mode cache-only my-app ../apps/my-app/logo.png ./scripts/download-icon.sh --mode required --url https://example.com/logo.png my-app ../apps/my-app/logo.png @@ -25,17 +31,21 @@ cd /root/github/1Panel-Appstore/skills `scripts/generate-app.sh` 支持 GitHub URL、compose URL、本地 compose 文件和 `docker run` 命令。 +GitHub URL 会按仓库默认分支、`main`、`master` 顺序尝试常见 compose 文件路径,例如 `docker-compose.yml`、`compose.yaml`、`deploy/docker-compose.yml`。如果没有找到 compose,会从 README 中尝试提取单行 `docker run` 命令作为草稿来源。 + 常用参数: ```text --output 输出目录,默认 ./apps --app-key 指定应用目录名 --name 指定应用显示名 +--service 指定多服务 compose 的主服务 --version 指定具体版本目录和镜像 tag --icon-mode auto|required|skip|cache-only --icon-url 使用指定图标 URL --force 允许覆盖已有生成目录 --dry-run 只解析并输出结果,不写文件 +--check-deps 只检查依赖工具 ``` ## 图标策略 diff --git a/skills/SKILL.md b/skills/SKILL.md index 946a2e2..5056227 100644 --- a/skills/SKILL.md +++ b/skills/SKILL.md @@ -37,9 +37,9 @@ Some apps also include `latest/`. When both `latest/` and a concrete version exi ## Workflow 1. Inspect the source deployment. - - GitHub: inspect README/deploy docs and registry image. + - GitHub: inspect README/deploy docs and registry image; the generator checks common compose paths before README `docker run` fallback. - Compose: parse services, image tags, ports, volumes, environment, dependencies. - - Docker run: parse image, `--name`, `-p`, `-v`, and `-e`. + - Docker run: parse image, `--name`, `-p/--publish`, `-v/--volume`, `-e/--env`, and `--env-file`. 2. Choose a stable app key. - Lowercase, hyphenated, directory-safe. - `additionalProperties.key` must match the directory name. @@ -95,11 +95,13 @@ Useful generation options: --output Output base directory. Default: ./apps --app-key Override app directory key. --name Override display name. +--service Select the main service from a multi-service compose file. --version Override concrete version and image tag. --icon-mode auto|required|skip|cache-only --icon-url Download icon from a known URL. --force Allow overwriting an existing generated directory. --dry-run Print parsed values without writing files. +--check-deps Check required local tools. ``` Download an icon: diff --git a/skills/examples/example-usage.md b/skills/examples/example-usage.md index 94a1235..2212552 100644 --- a/skills/examples/example-usage.md +++ b/skills/examples/example-usage.md @@ -8,6 +8,7 @@ cd /root/github/1Panel-Appstore/skills ./scripts/generate-app.sh \ --app-key demo-app \ --name DemoApp \ + --service web \ --version 1.25.3 \ --icon-mode skip \ --output ../apps \ @@ -45,10 +46,25 @@ cd /root/github/1Panel-Appstore/skills 脚本会解析: - `--name` 作为服务名候选。 -- `-p` 生成 `PANEL_APP_PORT_*` 表单字段。 -- `-v` 保留到 compose 的 `volumes`。 +- `-p/--publish` 生成 `PANEL_APP_PORT_*` 表单字段。 +- `-v/--volume` 保留到 compose 的 `volumes`。 +- `-e/--env` 和 `--env-file` 保留到 compose。 - 镜像 tag 作为版本候选;显式 `--version` 优先。 +## GitHub 输入 + +```bash +./scripts/generate-app.sh \ + --app-key github-demo \ + --name GitHubDemo \ + --service web \ + --version 1.2.3 \ + --icon-mode skip \ + https://github.com/example/demo +``` + +脚本会尝试仓库默认分支、`main`、`master` 下的常见 compose 路径;如果没有找到 compose,会从 README 中尝试提取单行 `docker run` 命令。生成结果仍需要人工审查。 + ## 图标处理 ```bash @@ -64,6 +80,7 @@ cd /root/github/1Panel-Appstore/skills ## 验证 ```bash +./scripts/generate-app.sh --check-deps ./scripts/validate-app.sh ../apps/demo-app ./tests/run_all.sh ``` diff --git a/skills/scripts/generate-app.sh b/skills/scripts/generate-app.sh index 5894e14..080f5f3 100755 --- a/skills/scripts/generate-app.sh +++ b/skills/scripts/generate-app.sh @@ -25,11 +25,17 @@ ICON_CACHE_DIR="${SCRIPT_DIR}/../.cache/icons" USER_APP_KEY="" USER_APP_NAME="" USER_VERSION="" +USER_SERVICE="" FORCE=false DRY_RUN=false +CHECK_DEPS_ONLY=false POSITIONAL=() SERVICE_VOLUMES=() SERVICE_ENV=() +SERVICE_ENV_FILES=() +DOCKER_RUN_CONTENT="" +GITHUB_API_BASE_URL="${GITHUB_API_BASE_URL:-https://api.github.com/repos}" +GITHUB_RAW_BASE_URL="${GITHUB_RAW_BASE_URL:-https://raw.githubusercontent.com}" # 打印帮助 print_help() { @@ -71,11 +77,13 @@ ${YELLOW}选项:${NC} --output <目录> 输出目录(默认: ./apps) --app-key 显式指定应用目录名 --name <名称> 显式指定应用显示名 + --service <服务名> 从 compose 中选择主服务 --version <版本> 显式指定具体版本目录和镜像 tag --icon-mode <模式> auto|required|skip|cache-only --icon-url 使用指定图标 URL --force 允许覆盖已有输出目录 --dry-run 只解析和打印结果,不写文件 + --check-deps 只检查脚本依赖 EOF } @@ -88,6 +96,25 @@ normalize_app_key() { echo "$1" | tr '[:upper:]' '[:lower:]' | tr ' _' '--' | tr -cd 'a-z0-9.-' } +check_dependencies() { + local missing=() + local dep + + for dep in curl jq yq python3; do + if ! command -v "$dep" >/dev/null 2>&1; then + missing+=("$dep") + fi + done + + if ((${#missing[@]} > 0)); then + log_error "缺少依赖: ${missing[*]}" + log_info "请先安装缺失工具后再运行 generate-app.sh" + return 1 + fi + + return 0 +} + parse_args() { POSITIONAL=() while [[ $# -gt 0 ]]; do @@ -104,6 +131,10 @@ parse_args() { USER_APP_NAME="${2:-}" shift 2 ;; + --service) + USER_SERVICE="${2:-}" + shift 2 + ;; --version) USER_VERSION="${2:-}" shift 2 @@ -124,6 +155,10 @@ parse_args() { DRY_RUN=true shift ;; + --check-deps) + CHECK_DEPS_ONLY=true + shift + ;; -h|--help) print_help exit 0 @@ -358,13 +393,13 @@ detect_input_type() { # 从 GitHub 项目提取信息 extract_from_github() { local github_url="$1" - local api_url temp_dir + local api_url log_info "正在从 GitHub 项目提取信息: $github_url" - # 转换为 API URL - # https://github.com/owner/repo -> https://api.github.com/repos/owner/repo - api_url="${github_url/github\.com/api.github.com\/repos}" + local owner_repo + owner_repo=$(echo "$github_url" | sed -E 's|https?://github.com/||; s|/$||; s|\.git$||') + api_url="${GITHUB_API_BASE_URL%/}/${owner_repo}" # 获取项目信息 local repo_info @@ -379,16 +414,96 @@ extract_from_github() { if [[ -z "$WEBSITE" ]]; then WEBSITE="$github_url" fi + DEFAULT_BRANCH=$(echo "$repo_info" | jq -r '.default_branch // "main"') - # 尝试查找 docker-compose.yml - local owner_repo - owner_repo=$(echo "$github_url" | sed -E 's|https?://github.com/||') - COMPOSE_URL="https://raw.githubusercontent.com/${owner_repo}/main/docker-compose.yml" + COMPOSE_CONTENT=$(discover_github_compose "$owner_repo" "$DEFAULT_BRANCH") + if [[ -z "${COMPOSE_CONTENT:-}" ]]; then + DOCKER_RUN_CONTENT=$(discover_github_docker_run "$owner_repo" "$DEFAULT_BRANCH") + fi + if [[ -z "${COMPOSE_CONTENT:-}" && -z "${DOCKER_RUN_CONTENT:-}" ]]; then + log_error "未在 GitHub 项目中发现常见 docker-compose 文件或 README docker run 命令" + exit 1 + fi log_info "应用名称: $APP_NAME" log_info "描述: $DESCRIPTION" } +discover_github_compose() { + local owner_repo="$1" + local default_branch="$2" + local branches=("$default_branch") + local branch path url content + local paths=( + "docker-compose.yml" + "docker-compose.yaml" + "compose.yml" + "compose.yaml" + "deploy/docker-compose.yml" + "deploy/docker-compose.yaml" + "docker/docker-compose.yml" + "docker/docker-compose.yaml" + ) + + if [[ "$default_branch" != "main" ]]; then + branches+=("main") + fi + if [[ "$default_branch" != "master" ]]; then + branches+=("master") + fi + + for branch in "${branches[@]}"; do + for path in "${paths[@]}"; do + url="${GITHUB_RAW_BASE_URL%/}/${owner_repo}/${branch}/${path}" + content=$(curl -fsSL --connect-timeout 5 --max-time 20 "$url" 2>/dev/null || true) + if [[ -n "$content" ]] && echo "$content" | yq -e '.services' >/dev/null 2>&1; then + log_info "发现 compose 文件: ${path} (${branch})" >&2 + printf '%s\n' "$content" + return 0 + fi + done + done + + return 0 +} + +discover_github_docker_run() { + local owner_repo="$1" + local default_branch="$2" + local branches=("$default_branch") + local branch path url content line + local paths=( + "README.md" + "README_en.md" + "docs/README.md" + ) + + if [[ "$default_branch" != "main" ]]; then + branches+=("main") + fi + if [[ "$default_branch" != "master" ]]; then + branches+=("master") + fi + + for branch in "${branches[@]}"; do + for path in "${paths[@]}"; do + url="${GITHUB_RAW_BASE_URL%/}/${owner_repo}/${branch}/${path}" + content=$(curl -fsSL --connect-timeout 5 --max-time 20 "$url" 2>/dev/null || true) + if [[ -z "$content" ]]; then + continue + fi + line=$(printf '%s\n' "$content" | sed -n 's/^[[:space:]]*//; /docker run /{s/^`*//; s/`*$//; p; q;}') + if [[ -n "$line" ]]; then + log_info "发现 README docker run 命令: ${path} (${branch})" >&2 + printf '%s\n' "$line" + return 0 + fi + done + done + + return 0 +} + # 从 docker-compose 提取信息 extract_from_compose() { local compose_content="$1" @@ -396,11 +511,23 @@ extract_from_compose() { log_info "正在解析 docker-compose 内容" # 提取服务名 - SERVICE_NAME=$(echo "$compose_content" | yq -r '.services | keys | .[0]' 2>/dev/null || echo "") + if [[ -n "$USER_SERVICE" ]]; then + SERVICE_NAME="$USER_SERVICE" + if ! echo "$compose_content" | yq -e ".services[\"${SERVICE_NAME}\"]" >/dev/null 2>&1; then + log_error "compose 中不存在指定服务: $SERVICE_NAME" + exit 1 + fi + else + SERVICE_NAME=$(echo "$compose_content" | yq -r '.services | keys | .[0]' 2>/dev/null || echo "") + fi # 提取镜像 local image image=$(echo "$compose_content" | yq -r ".services[\"${SERVICE_NAME}\"].image" 2>/dev/null || echo "") + if [[ -z "$image" || "$image" == "null" ]]; then + log_error "服务 ${SERVICE_NAME} 缺少 image,无法生成应用版本" + exit 1 + fi parse_image_ref "$image" # 提取端口 @@ -426,6 +553,12 @@ extract_from_compose() { SERVICE_ENV+=("$line") done < <(echo "$compose_content" | yq -r ".services[\"${SERVICE_NAME}\"].environment[]?" 2>/dev/null || true) + SERVICE_ENV_FILES=() + while IFS= read -r line; do + [[ -z "$line" || "$line" == "null" ]] && continue + SERVICE_ENV_FILES+=("$line") + done < <(echo "$compose_content" | yq -r ".services[\"${SERVICE_NAME}\"].env_file[]?" 2>/dev/null || true) + log_info "服务名: $SERVICE_NAME" log_info "镜像: $IMAGE_BASE:$TAG" log_info "端口数量: ${#PORT_ENTRIES[@]}" @@ -437,37 +570,104 @@ extract_from_docker_run() { log_info "正在解析 docker run 命令" - # 提取镜像 - local image_ref - image_ref=$(echo "$cmd" | awk '{print $NF}') - parse_image_ref "$image_ref" - - # 提取容器名 - SERVICE_NAME=$(echo "$cmd" | grep -oE '\-\-name(=| )[A-Za-z0-9_.-]+' | sed -E 's/^--name[= ]//' || echo "app") - - # 提取端口 - PORT_ENTRIES=() + local args=() while IFS= read -r line; do - [[ -z "$line" ]] && continue - PORT_ENTRIES+=("$line") - done < <(echo "$cmd" | grep -oE '\-p[= ]+[^ ]+' | awk '{print $NF}' || true) + args+=("$line") + done < <(python3 - "$cmd" <<'PY' +import shlex +import sys + +for item in shlex.split(sys.argv[1]): + print(item) +PY +) + + local i=0 + if [[ "${args[0]:-}" == "docker" && "${args[1]:-}" == "run" ]]; then + i=2 + fi + + SERVICE_NAME="app" + PORT_ENTRIES=() + SERVICE_VOLUMES=() + SERVICE_ENV=() + SERVICE_ENV_FILES=() + + local image_ref="" + while ((i < ${#args[@]})); do + local arg="${args[$i]}" + case "$arg" in + --name=*) + SERVICE_NAME="${arg#--name=}" + ;; + --name) + ((i+=1)) + SERVICE_NAME="${args[$i]:-app}" + ;; + -p|--publish) + ((i+=1)) + PORT_ENTRIES+=("${args[$i]:-}") + ;; + -p=*|--publish=*) + PORT_ENTRIES+=("${arg#*=}") + ;; + -v|--volume) + ((i+=1)) + SERVICE_VOLUMES+=("${args[$i]:-}") + ;; + -v=*|--volume=*) + SERVICE_VOLUMES+=("${arg#*=}") + ;; + -e|--env) + ((i+=1)) + SERVICE_ENV+=("${args[$i]:-}") + ;; + -e=*|--env=*) + SERVICE_ENV+=("${arg#*=}") + ;; + --env-file) + ((i+=1)) + SERVICE_ENV_FILES+=("${args[$i]:-}") + ;; + --env-file=*) + SERVICE_ENV_FILES+=("${arg#--env-file=}") + ;; + --) + ((i+=1)) + image_ref="${args[$i]:-}" + break + ;; + -d|--detach|--rm|--init|-i|-t|-it|-ti|--privileged) + ;; + --restart|--network|--hostname|--user|--workdir|--entrypoint|--add-host|--dns) + ((i+=1)) + ;; + --restart=*|--network=*|--hostname=*|--user=*|--workdir=*|--entrypoint=*|--add-host=*|--dns=*) + ;; + -*) + if [[ "$arg" != *=* && "${args[$((i + 1))]:-}" != -* && "${args[$((i + 1))]:-}" != "" ]]; then + ((i+=1)) + fi + ;; + *) + image_ref="$arg" + break + ;; + esac + ((i+=1)) + done + + if [[ -z "$image_ref" ]]; then + log_error "未能从 docker run 命令中解析出镜像" + exit 1 + fi + + parse_image_ref "$image_ref" if [[ ${#PORT_ENTRIES[@]} -eq 0 ]]; then PORT_ENTRIES=("8080") fi - SERVICE_VOLUMES=() - while IFS= read -r line; do - [[ -z "$line" ]] && continue - SERVICE_VOLUMES+=("$line") - done < <(echo "$cmd" | grep -oE '(-v|--volume)(=| )[^ ]+' | sed -E 's/^(-v|--volume)[= ]//' || true) - - SERVICE_ENV=() - while IFS= read -r line; do - [[ -z "$line" ]] && continue - SERVICE_ENV+=("$line") - done < <(echo "$cmd" | grep -oE '(-e|--env)(=| )[^ ]+' | sed -E 's/^(-e|--env)[= ]//' || true) - APP_NAME="$SERVICE_NAME" APP_KEY=$(normalize_app_key "$SERVICE_NAME") @@ -614,6 +814,18 @@ EOF EOF fi + if ((${#SERVICE_ENV_FILES[@]} > 0)); then + cat >> "$output_file" << EOF + env_file: +EOF + local env_file + for env_file in "${SERVICE_ENV_FILES[@]}"; do + cat >> "$output_file" << EOF + - ${env_file} +EOF + done + fi + cat >> "$output_file" << EOF image: ${IMAGE_BASE:-app}:${image_tag} labels: @@ -698,6 +910,12 @@ download_icon() { main() { parse_args "$@" + if [[ "$CHECK_DEPS_ONLY" == "true" ]]; then + check_dependencies + exit $? + fi + check_dependencies + local input="${POSITIONAL[0]:-}" local input_type @@ -717,9 +935,10 @@ main() { case "$input_type" in github) extract_from_github "$input" - COMPOSE_CONTENT=$(curl -s "$COMPOSE_URL" 2>/dev/null || echo "") if [[ -n "$COMPOSE_CONTENT" ]]; then extract_from_compose "$COMPOSE_CONTENT" + elif [[ -n "$DOCKER_RUN_CONTENT" ]]; then + extract_from_docker_run "$DOCKER_RUN_CONTENT" fi ;; compose_url) diff --git a/skills/tests/test_skills_scripts.sh b/skills/tests/test_skills_scripts.sh index 8bf77d9..ea0898c 100755 --- a/skills/tests/test_skills_scripts.sh +++ b/skills/tests/test_skills_scripts.sh @@ -25,6 +25,19 @@ assert_contains() { grep -qE -- "$pattern" "$file" || fail "expected pattern '$pattern' in $file" } +make_fake_path_without() { + local dir="$1" + shift + mkdir -p "$dir" + + local tool + for tool in awk basename cat chmod cmp cp dirname env find grep head mkdir mktemp mv printf rm sed sort tail tr; do + if command -v "$tool" >/dev/null 2>&1; then + ln -s "$(command -v "$tool")" "$dir/$tool" + fi + done +} + run_test() { local name="$1" shift @@ -32,6 +45,19 @@ run_test() { "$@" } +test_generate_app_reports_missing_dependencies() { + local fake_path="$TMP_DIR/fake-path" + make_fake_path_without "$fake_path" + + if PATH="$fake_path" /usr/bin/bash "$ROOT_DIR/skills/scripts/generate-app.sh" --check-deps >/tmp/generate_missing_deps.out 2>&1; then + fail "dependency check should fail when yq/jq/curl are missing" + fi + assert_contains /tmp/generate_missing_deps.out "缺少依赖" + assert_contains /tmp/generate_missing_deps.out "yq" + assert_contains /tmp/generate_missing_deps.out "jq" + assert_contains /tmp/generate_missing_deps.out "curl" +} + test_download_icon_skip_mode_does_not_create_logo() { local out="$TMP_DIR/skip/logo.png" @@ -109,6 +135,125 @@ YAML assert_contains "$output/demo-preserve/1.25.3/docker-compose.yml" "DEMO_FLAG=true" } +test_generate_app_uses_explicit_compose_service() { + local compose="$TMP_DIR/compose-service.yml" + local output="$TMP_DIR/generated-service" + cat > "$compose" <<'YAML' +services: + db: + image: postgres:16 + ports: + - "15432:5432" + web: + image: ghcr.io/example/webapp:2.3.4 + ports: + - "18080:8080" + environment: + - WEB_MODE=prod +YAML + + bash "$ROOT_DIR/skills/scripts/generate-app.sh" \ + --service web \ + --app-key demo-service \ + --name DemoService \ + --version 2.3.4 \ + --output "$output" \ + --icon-mode skip \ + "$compose" >/tmp/generate_app_service.out + + assert_contains "$output/demo-service/2.3.4/docker-compose.yml" "web:" + assert_contains "$output/demo-service/2.3.4/docker-compose.yml" "image: ghcr.io/example/webapp:2.3.4" + assert_contains "$output/demo-service/2.3.4/docker-compose.yml" "8080" + assert_contains "$output/demo-service/2.3.4/data.yml" "default: 18080" + assert_contains "$output/demo-service/2.3.4/docker-compose.yml" "WEB_MODE=prod" +} + +test_generate_app_discovers_github_default_branch_compose() { + local fixture="$TMP_DIR/github-fixture" + local output="$TMP_DIR/generated-github" + mkdir -p "$fixture/api/repos/example" "$fixture/raw/example/demo/master" + + cat > "$fixture/api/repos/example/demo" <<'JSON' +{ + "name": "demo", + "description": "Demo GitHub app", + "homepage": "https://demo.example", + "default_branch": "master" +} +JSON + cat > "$fixture/raw/example/demo/master/compose.yaml" <<'YAML' +services: + demo: + image: nginx:1.25.3 + ports: + - "18080:80" +YAML + + GITHUB_API_BASE_URL="file://${fixture}/api/repos" \ + GITHUB_RAW_BASE_URL="file://${fixture}/raw" \ + bash "$ROOT_DIR/skills/scripts/generate-app.sh" \ + --app-key github-demo \ + --version 1.25.3 \ + --output "$output" \ + --icon-mode skip \ + "https://github.com/example/demo" >/tmp/generate_app_github.out + + assert_contains "$output/github-demo/data.yml" "Demo GitHub app" + assert_contains "$output/github-demo/1.25.3/docker-compose.yml" "image: nginx:1.25.3" +} + +test_generate_app_discovers_github_readme_docker_run() { + local fixture="$TMP_DIR/github-readme-fixture" + local output="$TMP_DIR/generated-github-readme" + mkdir -p "$fixture/api/repos/example" "$fixture/raw/example/readme-demo/main" + + cat > "$fixture/api/repos/example/readme-demo" <<'JSON' +{ + "name": "readme-demo", + "description": "README docker run app", + "homepage": "", + "default_branch": "main" +} +JSON + cat > "$fixture/raw/example/readme-demo/main/README.md" <<'MD' +# README Demo + +```bash +docker run -d --name readme-demo -p 18080:80 -e "APP_TITLE=Readme Demo" nginx:1.25.3 +``` +MD + + GITHUB_API_BASE_URL="file://${fixture}/api/repos" \ + GITHUB_RAW_BASE_URL="file://${fixture}/raw" \ + bash "$ROOT_DIR/skills/scripts/generate-app.sh" \ + --app-key github-readme-demo \ + --version 1.25.3 \ + --output "$output" \ + --icon-mode skip \ + "https://github.com/example/readme-demo" >/tmp/generate_app_github_readme.out + + assert_contains "$output/github-readme-demo/1.25.3/docker-compose.yml" "image: nginx:1.25.3" + assert_contains "$output/github-readme-demo/1.25.3/docker-compose.yml" "APP_TITLE=Readme Demo" + assert_contains "$output/github-readme-demo/1.25.3/data.yml" "default: 18080" +} + +test_generate_app_parses_complex_docker_run_flags() { + local output="$TMP_DIR/generated-run" + + bash "$ROOT_DIR/skills/scripts/generate-app.sh" \ + --app-key docker-run-demo \ + --name DockerRunDemo \ + --version 1.2.3 \ + --output "$output" \ + --icon-mode skip \ + 'docker run -d --name docker-run-demo --env "APP_TITLE=My Demo" --env-file ./data/app.env --publish=127.0.0.1:18080:8080/tcp --volume=./data/demo:/data ghcr.io/example/demo:1.2.3' >/tmp/generate_app_run.out + + assert_contains "$output/docker-run-demo/1.2.3/docker-compose.yml" "image: ghcr.io/example/demo:1.2.3" + assert_contains "$output/docker-run-demo/1.2.3/docker-compose.yml" "APP_TITLE=My Demo" + assert_contains "$output/docker-run-demo/1.2.3/docker-compose.yml" "./data/demo:/data" + assert_contains "$output/docker-run-demo/1.2.3/docker-compose.yml" "8080" +} + test_validate_app_rejects_undefined_port_variable() { local app="$TMP_DIR/bad-port/apps/demo-app" mkdir -p "$app/1.0.0" @@ -203,8 +348,13 @@ YAML run_test "download icon skip mode" test_download_icon_skip_mode_does_not_create_logo run_test "download icon cache-only mode" test_download_icon_cache_only_uses_cached_logo +run_test "generate app dependency check" test_generate_app_reports_missing_dependencies run_test "generate app explicit options" test_generate_app_accepts_explicit_options_and_skips_icon run_test "generate app preserves compose fields" test_generate_app_preserves_compose_environment_and_volumes +run_test "generate app explicit compose service" test_generate_app_uses_explicit_compose_service +run_test "generate app github compose discovery" test_generate_app_discovers_github_default_branch_compose +run_test "generate app github README docker run discovery" test_generate_app_discovers_github_readme_docker_run +run_test "generate app complex docker run flags" test_generate_app_parses_complex_docker_run_flags run_test "validate undefined port variable" test_validate_app_rejects_undefined_port_variable run_test "validate latest image tag" test_validate_app_rejects_latest_wrong_tag