From dbb6bbce021a1213ae6d36ca04434e564e097ec6 Mon Sep 17 00:00:00 2001 From: easyai03 Date: Thu, 15 Oct 2026 03:09:51 +0800 Subject: [PATCH] =?UTF-8?q?fix(release):=20=E6=8C=89=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E6=A0=B8=E9=AA=8C=20Compose=20=E5=9B=BA=E5=AE=9A=E9=95=9C?= =?UTF-8?q?=E5=83=8F=E5=B9=B6=E4=BF=9D=E7=95=99=E7=BA=BF=E4=B8=8A=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/Initialize-ApplicationVersions.ps1 | 13 ++++-- scripts/init-application-version-env.sh | 15 ++++++- scripts/test-application-version-images.py | 51 ++++++++++++++++++++++ scripts/test-model-conversion-update.py | 5 ++- 4 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 scripts/test-application-version-images.py diff --git a/scripts/Initialize-ApplicationVersions.ps1 b/scripts/Initialize-ApplicationVersions.ps1 index 1d58672..f8671e6 100644 --- a/scripts/Initialize-ApplicationVersions.ps1 +++ b/scripts/Initialize-ApplicationVersions.ps1 @@ -52,10 +52,17 @@ function Test-ApplicationVersionImages { param([object[]]$Plan, [string[]]$ComposeCommand) foreach ($item in $Plan) { if (-not $item.Service) { continue } - $args = @($ComposeCommand | Select-Object -Skip 1) + @('config','--images',$item.Service) + $args = @($ComposeCommand | Select-Object -Skip 1) + @('config','--format','json',$item.Service) $executable = $ComposeCommand[0] - $actual = & $executable @args - if ($LASTEXITCODE -ne 0 -or "$actual".Trim() -ne "$($item.Image):$($item.Version)") { + $configuration = & $executable @args + if ($LASTEXITCODE -ne 0) { throw '无法读取应用的有效镜像定义;尚未拉取或重建服务' } + try { + # Compose includes dependencies even when a service is selected. + # Keep the configuration in memory; do not log its environment. + $resolved = ($configuration -join "`n") | ConvertFrom-Json -ErrorAction Stop + $actual = $resolved.services.PSObject.Properties[$item.Service].Value.image + } catch { throw '无法读取应用的有效镜像定义;尚未拉取或重建服务' } + if ("$actual".Trim() -ne "$($item.Image):$($item.Version)") { throw "$($item.Service) 的有效镜像未使用环境中的固定版本;请通过部署仓库的受控迁移恢复镜像定义,尚未拉取或重建服务" } } diff --git a/scripts/init-application-version-env.sh b/scripts/init-application-version-env.sh index ca66813..6625706 100644 --- a/scripts/init-application-version-env.sh +++ b/scripts/init-application-version-env.sh @@ -58,11 +58,22 @@ apply_application_versions() { } verify_application_version_images() { - local file="$1" compose="$2" service key option image version actual + local file="$1" compose="$2" service key option image version actual configuration while read -r service key option image; do application_selected "$service" || continue version="$(read_env_value "$file" "$key")" - actual="$(sudo $compose config --images "$service")" || return 1 + # --images SERVICE includes dependency images on Compose 5. Read the + # selected service from normalized config instead of comparing the list. + configuration="$(sudo $compose config --format json "$service")" || return 1 + actual="$(printf '%s' "$configuration" | python3 -c ' +import json, sys +try: + image = json.load(sys.stdin)["services"][sys.argv[1]]["image"] + if not isinstance(image, str): raise ValueError() + print(image) +except (ValueError, KeyError, TypeError): + raise SystemExit("无法读取应用的有效镜像定义;尚未拉取或重建服务") +' "$service")" || return 1 if [ "$actual" != "$image:$version" ]; then echo "❌ $service 的有效镜像未使用 .env 中的固定版本 $key=$version;请通过部署仓库的受控迁移恢复镜像定义,尚未拉取或重建服务" >&2 return 1 diff --git a/scripts/test-application-version-images.py b/scripts/test-application-version-images.py new file mode 100644 index 0000000..642ac82 --- /dev/null +++ b/scripts/test-application-version-images.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +"""Exercise Bash and PowerShell image verification with real Compose config. + +No images are pulled and no containers are created. Fixture settings are fake. +""" +import argparse +import json +import os +from pathlib import Path +import subprocess +import tempfile +import time + +parser = argparse.ArgumentParser() +parser.add_argument('--source', type=Path, default=Path(__file__).resolve().parent.parent) +args = parser.parse_args() +source = args.source.resolve() +started = time.monotonic() +version = '1.0.0.20261014' +image = 'registry.cn-shanghai.aliyuncs.com/comfy-ai/comfy-server' +secret = 'synthetic-config-value-must-not-be-printed' + +with tempfile.TemporaryDirectory(prefix='easyai-image-config-') as temporary: + root = Path(temporary) + (root / '.env').write_text(f'SERVER_VERSION={version}\nFIXTURE_SECRET={secret}\n') + binaries = root / 'bin' + binaries.mkdir() + sudo = binaries / 'sudo' + sudo.write_text('#!/bin/sh\nexec "$@"\n') + sudo.chmod(0o755) + env = {**os.environ, 'PATH': str(binaries) + os.pathsep + os.environ['PATH'], 'UPDATE_SERVICES': 'easyai-server'} + reports = [] + for correct in (True, False): + selected = f'{image}:${{SERVER_VERSION}}' if correct else f'{image}:latest' + # On the negative case a dependency deliberately has the expected image; + # checking membership in --images output would wrongly approve it. + dependency = 'redis:7-alpine' if correct else f'{image}:${{SERVER_VERSION}}' + (root / 'compose.yml').write_text(f'services:\n dependency:\n image: {dependency}\n easyai-server:\n image: {selected}\n depends_on:\n - dependency\n environment:\n FIXTURE_SECRET: ${{FIXTURE_SECRET}}\n') + config = subprocess.run(['docker','compose','config','--format','json','easyai-server'], cwd=root, env=env, text=True, capture_output=True, check=True) + assert json.loads(config.stdout)['services']['easyai-server']['image'] == (f'{image}:{version}' if correct else f'{image}:latest') + images = subprocess.run(['docker','compose','config','--images','easyai-server'], cwd=root, env=env, text=True, capture_output=True, check=True) + assert len(images.stdout.splitlines()) > 1, 'Compose fixture must include dependency images' + bash = ['bash','-c', '. "$1/scripts/init-security-env.sh"; . "$1/scripts/init-application-version-env.sh"; verify_application_version_images .env "docker compose"', 'fixture', str(source)] + powershell = ['pwsh','-NoProfile','-NonInteractive','-Command', f". '{source}/scripts/Initialize-ApplicationVersions.ps1'; Test-ApplicationVersionImages @([pscustomobject]@{{Service='easyai-server';Image='{image}';Version='{version}'}}) @('docker','compose')"] + for name, command in [('bash', bash), ('powershell', powershell)]: + result = subprocess.run(command, cwd=root, env=env, text=True, errors='backslashreplace', capture_output=True, timeout=30) + assert (result.returncode == 0) == correct, f'{name} incorrect verification: {result.stdout} {result.stderr}' + assert secret not in result.stdout + result.stderr, 'configuration leaked' + reports.append({'shell':name, 'correctImage':correct, 'expectedExitObserved':True}) + +print(json.dumps({'cases':reports,'seconds':round(time.monotonic()-started,3)})) diff --git a/scripts/test-model-conversion-update.py b/scripts/test-model-conversion-update.py index 75a18b3..0ea5fec 100644 --- a/scripts/test-model-conversion-update.py +++ b/scripts/test-model-conversion-update.py @@ -57,11 +57,12 @@ if args[:1] == ['inspect']: sys.exit(0) sys.exit(1) if args[:3] == ['compose', 'config', '--services']: print('easyai-server\\neasyai-web\\nws-gateway\\neasyai-updater\\nmodel-conversion-worker\\nmongo\\nredis') -elif args[:3] == ['compose', 'config', '--images']: +elif args[:4] == ['compose', 'config', '--format', 'json']: names = {'easyai-server':('SERVER_VERSION','comfy-ai/comfy-server'), 'easyai-web':('WEB_VERSION','comfy-ai/one-ai'), 'ws-gateway':('WS_GATEWAY_VERSION','easyaigc/wsgateway'), 'easyai-updater':('UPDATER_VERSION','easyaigc/easyai-updater'), 'model-conversion-worker':('MODEL_CONVERSION_WORKER_VERSION','easyaigc/model-conversion-worker')} values = dict(line.split('=',1) for line in open('.env').read().splitlines() if '=' in line and not line.startswith('#')) key,image = names[args[-1]] - print('registry.cn-shanghai.aliyuncs.com/'+image+':'+('latest' if os.environ.get('UPDATE_TEST_IMAGE_OVERRIDE') == '1' else values[key])) + actual = 'registry.cn-shanghai.aliyuncs.com/'+image+':'+('latest' if os.environ.get('UPDATE_TEST_IMAGE_OVERRIDE') == '1' else values[key]) + print(json.dumps({'services': {args[-1]: {'image':actual}, 'dependency': {'image':'redis:7-alpine'}}})) elif args[:3] == ['compose', 'ps', '-q']: print('container-' + args[-1]) sys.exit(0) ''')