diff --git a/README.md b/README.md index 52c99a4..677472e 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Discussion and contributions are welcome: ## Documentation & Tutorial -[Wiki & Document](https://labnow.ai/) | [中文使用指引(含中国网络镜像)](https://labnow-ai.feishu.cn/wiki/wikcn0sBhMtb1KNRSUTettxWstc) +[Wiki & Document](https://doc.labnow.ai/) | [中文使用指引(含中国网络镜像)](https://doc.labnow.ai/zh-CN/) ## Develop and Debug diff --git a/docker_openclaw/demo/docker-compose.yml b/docker_openclaw/demo/docker-compose.yml index e7cc0f1..b44f283 100644 --- a/docker_openclaw/demo/docker-compose.yml +++ b/docker_openclaw/demo/docker-compose.yml @@ -4,32 +4,16 @@ services: openclaw-gateway: container_name: svc-openclaw-gateway hostname: svc-openclaw-gateway - image: "quay.io/labnow0dev/openclaw:latest" + image: "quay.io/labnow/openclaw:latest" pull_policy: if_not_present restart: unless-stopped environment: - TZ=Asia/Shanghai - PROFILE_LOCALIZE=aliyun-pub + - OPENCLAW_GATEWAY_TOKEN=openclaw volumes: - /data/openclaw:/opt/openclaw/data ports: - "${OPENCLAW_GATEWAY_PORT:-18789}:18789" - "${OPENCLAW_BRIDGE_PORT:-18790}:18790" - init: true - - openclaw-cli: - container_name: svc-openclaw-cli - hostname: svc-openclaw-cli - image: "quay.io/labnow0dev/openclaw:latest" - pull_policy: if_not_present - restart: "no" - environment: - - TZ=Asia/Shanghai - - PROFILE_LOCALIZE=aliyun-pub - - BROWSER=echo - volumes: - - /data/openclaw:/opt/openclaw/data - init: true - stdin_open: true - tty: true - entrypoint: ["node", "openclaw.mjs"] + # command: ["tail", "-f", "/dev/null"] diff --git a/docker_openclaw/openclaw.Dockerfile b/docker_openclaw/openclaw.Dockerfile index c569dd2..9398a48 100644 --- a/docker_openclaw/openclaw.Dockerfile +++ b/docker_openclaw/openclaw.Dockerfile @@ -7,28 +7,59 @@ FROM ${BASE_NAMESPACE:+$BASE_NAMESPACE/}${BASE_IMG} LABEL maintainer="postmaster@labnow.ai" ENV NODE_ENV=production ENV PNPM_HOME=/opt/node/pnpm -ENV PNPM_STORE_DIR=/opt/node/pnpm-store -ENV PNPM_NODE_LINKER=hoisted -ENV PATH="${PNPM_HOME}:${PATH}" +ENV PNPM_STORE=/opt/node/pnpm/store +ENV OPENCLAW_HOME=/opt/openclaw +ENV OPENCLAW_PLUGINS_ROOT=${OPENCLAW_HOME}/plugins +ENV OPENCLAW_CONFIG=${OPENCLAW_HOME}/.openclaw/openclaw.json +ENV PATH="${PNPM_HOME}:${OPENCLAW_HOME}:${PATH}" +ENV HOME=/opt/openclaw COPY work /opt/openclaw/ RUN set -eux && source /opt/utils/script-setup.sh \ - && chmod +x /opt/openclaw/start-openclaw.sh && ln -sf /opt/openclaw/start-openclaw.sh /usr/local/bin/ \ + && chmod +x /opt/openclaw/*.sh && ln -sf /opt/openclaw/start-openclaw.sh /usr/local/bin/ \ && mkdir -pv /opt/openclaw/data \ && ln -sfn /opt/openclaw/data /opt/openclaw/.openclaw \ ## curl -fsSL https://openclaw.ai/install.sh | NO_PROMPT=1 bash -s -- --no-onboard --install-method npm \ && export SHARP_IGNORE_GLOBAL_LIBVIPS=1 \ && setup_node_pnpm 10 \ && pnpm config set enable-pre-post-scripts true \ - && pnpm install -g openclaw@latest --ignore-scripts=false \ - && openclaw --version \ - ## Clean up and display components version information... - && list_installed_packages && install__clean + && pnpm config set package-import-method hardlink \ + && pnpm config set node-linker isolated \ + && pnpm config set store-dir $PNPM_STORE \ + && GLOBAL_DIR=$(pnpm root -g | sed 's|/node_modules$||') \ + && mkdir -pv "$GLOBAL_DIR" \ + && echo '{"dependencies":{},"pnpm":{"onlyBuiltDependencies":["@matrix-org/matrix-sdk-crypto-nodejs","koffi","openclaw","protobufjs","sharp"]}}' \ + | tee "$GLOBAL_DIR/package.json" \ + && pnpm config list \ + && pnpm install --prod -g --ignore-scripts=false --config.unsafe-perm=true --store-dir "$PNPM_STORE" openclaw@latest \ + && pnpm store prune --store-dir "$PNPM_STORE" && rm -rf "$PNPM_STORE" && install__clean \ + && openclaw --version + +RUN set -eux && source /opt/utils/script-utils.sh \ + && source /opt/openclaw/script-setup-openclaw.sh \ + && cd $OPENCLAW_HOME \ + && printf 'packages:\n - "plugins/*"\n' > pnpm-workspace.yaml \ + && printf '{"name":"openclaw-root","version":"1.0.0","private":true}\n' > package.json \ + && PNPM_VER="$(pnpm --version)" \ + && jq --arg ver "$PNPM_VER" \ + --argjson deps '["koffi","sharp","openclaw","protobufjs","@matrix-org/matrix-sdk-crypto-nodejs"]' \ + '. + {dependencies: {openclaw:"latest"}, packageManager: ("pnpm@" + $ver), pnpm: { onlyBuiltDependencies: $deps } }' package.json > package.tmp.json \ + && mv package.tmp.json package.json \ + && add_plugin "@larksuite/openclaw-lark" "openclaw-lark" \ + && pnpm install --prod \ + ## clean up + && pnpm store prune --store-dir "$PNPM_STORE" && rm -rf "$PNPM_STORE" && install__clean \ + && rm -rf ~/.* \ + && ln -sfn /opt/openclaw/data /opt/openclaw/.openclaw \ + && ls -alh ~/ -ENV HOME=/opt/openclaw/ ENV XDG_CONFIG_HOME=/opt/openclaw/data +ENV OPENCLAW_HIDE_BANNER=1 WORKDIR /opt/openclaw -VOLUME ["/opt/openclaw/data"] +VOLUME ["/opt/openclaw/data", "/opt/node/pnpm/store"] EXPOSE 18789 18790 -CMD ["sh", "start-openclaw.sh", "gateway", "--allow-unconfigured", "--bind", "${OPENCLAW_GATEWAY_BIND:-lan}", "--port", "${OPENCLAW_GATEWAY_PORT:-18789}"] + +CMD start-openclaw.sh gateway --allow-unconfigured \ + --bind "${OPENCLAW_GATEWAY_BIND:-lan}" \ + --port "${OPENCLAW_GATEWAY_PORT:-18789}" diff --git a/docker_openclaw/work/openclaw-plugin-installer.js b/docker_openclaw/work/openclaw-plugin-installer.js deleted file mode 100644 index b5bcdcd..0000000 --- a/docker_openclaw/work/openclaw-plugin-installer.js +++ /dev/null @@ -1,368 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -const fs = require('fs'); -const path = require('path'); -const { spawnSync } = require('child_process'); - -function buildDefaultConfig() { - return { - agents: { - defaults: { - workspace: '/opt/openclaw/data/workspace' - } - }, - channels: { - feishu: { - enabled: true, - appId: '', - appSecret: '', - domain: 'feishu', - connectionMode: 'websocket', - requireMention: true, - dmPolicy: 'pairing', - groupPolicy: 'open', - allowFrom: [], - groupAllowFrom: [] - } - }, - gateway: { - controlUi: { - dangerouslyAllowHostHeaderOriginFallback: true, - dangerouslyDisableDeviceAuth: true - } - } - }; -} - -function parseArgs(argv) { - const args = { _: [] }; - for (let i = 0; i < argv.length; i += 1) { - const token = argv[i]; - if (!token.startsWith('--')) { - args._.push(token); - continue; - } - - const eqIndex = token.indexOf('='); - if (eqIndex > -1) { - const key = token.slice(2, eqIndex); - const value = token.slice(eqIndex + 1); - args[key] = value; - continue; - } - - const key = token.slice(2); - const next = argv[i + 1]; - if (!next || next.startsWith('--')) { - args[key] = true; - continue; - } - - args[key] = next; - i += 1; - } - - return args; -} - -function parseCommaList(value) { - if (!value || value === true) return []; - return String(value) - .split(',') - .map((item) => item.trim()) - .filter(Boolean); -} - -function pluginIdFromPackage(packageName) { - return packageName.split('/').pop(); -} - -function ensureConfigShape(config) { - if (!config.plugins) config.plugins = {}; - if (!Array.isArray(config.plugins.allow)) config.plugins.allow = []; - if (!config.plugins.entries) config.plugins.entries = {}; -} - -function loadConfig(configPath) { - const raw = fs.readFileSync(configPath, 'utf8'); - return JSON.parse(raw); -} - -function saveConfig(configPath, config) { - fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8'); -} - -function ensureConfigFile(configPath) { - fs.mkdirSync(path.dirname(configPath), { recursive: true }); - if (fs.existsSync(configPath) && fs.statSync(configPath).size > 0) { - return; - } - saveConfig(configPath, buildDefaultConfig()); -} - -function mergeDefaultConfig(configPath) { - const config = loadConfig(configPath); - - if (!config.agents) config.agents = {}; - if (!config.agents.defaults) config.agents.defaults = {}; - if (!config.agents.defaults.workspace) { - config.agents.defaults.workspace = '/opt/openclaw/data/workspace'; - } - - if (!config.gateway) config.gateway = {}; - if (!config.gateway.controlUi) config.gateway.controlUi = {}; - if (typeof config.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback !== 'boolean') { - config.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback = true; - } - if (typeof config.gateway.controlUi.dangerouslyDisableDeviceAuth !== 'boolean') { - config.gateway.controlUi.dangerouslyDisableDeviceAuth = true; - } - - saveConfig(configPath, config); -} - -function runOpenclawInstall(packageName, env) { - const result = spawnSync('openclaw', ['plugins', 'install', packageName], { - encoding: 'utf8', - env - }); - - if (result.stdout) process.stdout.write(result.stdout); - if (result.stderr) process.stderr.write(result.stderr); - - if (result.status === 0) { - return { ok: true, alreadyExists: false }; - } - - const output = `${result.stdout || ''}\n${result.stderr || ''}`; - if (output.includes('plugin already exists')) { - return { ok: true, alreadyExists: true }; - } - - const err = new Error(`Command failed: openclaw plugins install ${packageName}`); - err.status = result.status; - err.output = output; - throw err; -} - -function safeRemove(targetPath) { - if (!fs.existsSync(targetPath)) return; - const stat = fs.lstatSync(targetPath); - if (stat.isDirectory() && !stat.isSymbolicLink()) { - fs.rmSync(targetPath, { recursive: true, force: true }); - } else { - fs.unlinkSync(targetPath); - } -} - -function isSelfReferencingSymlink(targetPath) { - try { - const stat = fs.lstatSync(targetPath); - if (!stat.isSymbolicLink()) return false; - const linkValue = fs.readlinkSync(targetPath); - const resolved = path.resolve(path.dirname(targetPath), linkValue); - return resolved === path.resolve(targetPath); - } catch (_err) { - return false; - } -} - -function ensurePluginAtTarget(options) { - const { homeClaw, stateDir, extensionsDir, pluginDirName } = options; - const targetPath = path.join(extensionsDir, pluginDirName); - const resolvedTargetPath = path.resolve(targetPath); - - if (isSelfReferencingSymlink(targetPath)) { - safeRemove(targetPath); - } - - if (fs.existsSync(targetPath)) { - return targetPath; - } - - const candidates = [ - path.join(stateDir, 'extensions', pluginDirName), - path.join(homeClaw, 'extensions', pluginDirName), - ]; - - const actualPath = candidates.find((candidate) => fs.existsSync(candidate)); - if (!actualPath) { - return ''; - } - const resolvedActualPath = path.resolve(actualPath); - if (resolvedActualPath === resolvedTargetPath) { - return targetPath; - } - - fs.mkdirSync(path.dirname(targetPath), { recursive: true }); - - try { - fs.renameSync(actualPath, targetPath); - } catch (err) { - if (!err || err.code !== 'EXDEV') throw err; - fs.cpSync(actualPath, targetPath, { recursive: true }); - safeRemove(actualPath); - } - - return targetPath; -} - -function applyInstallConfig(configPath, pluginId) { - const config = loadConfig(configPath); - ensureConfigShape(config); - - if (!config.plugins.allow.includes(pluginId)) { - config.plugins.allow.push(pluginId); - } - config.plugins.entries[pluginId] = { enabled: true }; - - saveConfig(configPath, config); -} - -function applyDisableConfig(configPath, entryIds) { - const config = loadConfig(configPath); - ensureConfigShape(config); - - for (const entryId of entryIds) { - config.plugins.entries[entryId] = { enabled: false }; - } - - saveConfig(configPath, config); -} - -function installCommand(args) { - const packageName = args.package; - const configPath = args.config; - const homeClaw = path.resolve(args.home || '/opt/openclaw'); - const stateDir = path.resolve(args['state-dir'] || path.dirname(configPath || '')); - const extensionsDir = path.resolve(args['extensions-dir'] || path.join(stateDir, 'extensions')); - const pluginId = args.id || pluginIdFromPackage(packageName || ''); - const pluginDirName = args['plugin-dir'] || pluginId; - const forceInstall = Boolean(args.force); - - if (!packageName || !configPath) { - throw new Error('Missing required args: --package, --config'); - } - - ensureConfigFile(configPath); - mergeDefaultConfig(configPath); - - fs.mkdirSync(stateDir, { recursive: true }); - fs.mkdirSync(extensionsDir, { recursive: true }); - - const targetPath = path.join(extensionsDir, pluginDirName); - const installEnv = { - ...process.env, - XDG_CONFIG_HOME: stateDir, - OPENCLAW_DIR_STATE: stateDir, - }; - - if (forceInstall || !fs.existsSync(targetPath)) { - console.log(`[plugin-installer] Installing ${packageName} ...`); - const result = runOpenclawInstall(packageName, installEnv); - if (result.alreadyExists) { - console.log(`[plugin-installer] ${pluginId} already exists, treat as idempotent success.`); - } - } else { - console.log(`[plugin-installer] ${pluginId} already exists at ${targetPath}, skip install.`); - } - - const finalPath = ensurePluginAtTarget({ homeClaw, stateDir, extensionsDir, pluginDirName }); - if (!finalPath) { - console.warn(`[plugin-installer] WARN: unable to locate installed plugin ${pluginId}.`); - } else { - console.log(`[plugin-installer] plugin path: ${finalPath}`); - } - - applyInstallConfig(configPath, pluginId); - console.log(`[plugin-installer] ${packageName} install flow completed.`); -} - -function disableCommand(args) { - const configPath = args.config; - const entryIds = parseCommaList(args.entry || args.entries); - - if (!configPath || entryIds.length === 0) { - throw new Error('Missing required args: --config, --entry '); - } - - ensureConfigFile(configPath); - mergeDefaultConfig(configPath); - applyDisableConfig(configPath, entryIds); - console.log(`[plugin-installer] disabled entries: ${entryIds.join(', ')}`); -} - -function initConfigCommand(args) { - const configPath = args.config; - if (!configPath) { - throw new Error('Missing required args: --config'); - } - - ensureConfigFile(configPath); - mergeDefaultConfig(configPath); - console.log(`[plugin-installer] config ready: ${configPath}`); -} - -function printHelp() { - console.log(`Usage: - node openclaw-plugin-installer.js [options] - -Commands: - init-config Ensure config file exists and apply default base settings - install Install plugin package and enable corresponding plugin entry - disable Disable plugin entries in config - -Common options: - --config OpenClaw config path - -Install options: - --package NPM package, e.g. @larksuite/openclaw-lark - --id Optional plugin id (default: package basename) - --home OpenClaw home (default: /opt/openclaw) - --state-dir State dir (default: dirname(--config)) - --extensions-dir Extensions dir (default: /extensions) - --plugin-dir Plugin directory name (default: plugin id) - --force Force install even if target plugin dir exists - -Disable options: - --entry Comma separated plugin entry ids to disable -`); -} - -function main() { - const args = parseArgs(process.argv.slice(2)); - const command = args._[0]; - - if (!command || command === 'help' || command === '--help') { - printHelp(); - process.exit(command ? 0 : 1); - } - - if (command === 'init-config') { - initConfigCommand(args); - return; - } - - if (command === 'install') { - installCommand(args); - return; - } - - if (command === 'disable') { - disableCommand(args); - return; - } - - throw new Error(`Unsupported command: ${command}`); -} - -try { - main(); -} catch (err) { - console.error(`[plugin-installer] ${err.message}`); - if (err.output) { - console.error(err.output); - } - process.exit(1); -} diff --git a/docker_openclaw/work/script-setup-openclaw.sh b/docker_openclaw/work/script-setup-openclaw.sh new file mode 100644 index 0000000..72c993e --- /dev/null +++ b/docker_openclaw/work/script-setup-openclaw.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +set -eu + + +init_config() { + if [ ! -f "$OPENCLAW_CONFIG" ]; then + mkdir -p "$(dirname "$OPENCLAW_CONFIG")" + + jq -n \ + --argjson plugin_paths "[\"$OPENCLAW_PLUGINS_ROOT\"]" \ + --arg token "${OPENCLAW_GATEWAY_TOKEN:-openclaw}" \ + '{ + plugins: { + load: { paths: $plugin_paths }, + entries: {} + }, + gateway: { + controlUi: { + dangerouslyAllowHostHeaderOriginFallback: true, + dangerouslyDisableDeviceAuth: true + }, + auth: { + mode: "token", + token: $token + } + } + }' > "$OPENCLAW_CONFIG" + fi +} + +list_downloaded_plugins() { + local plugins=() + + for plugin_dir in "$OPENCLAW_PLUGINS_ROOT"/*/; do + [[ -d "$plugin_dir" ]] || continue + local plugin + plugin=$(basename "$plugin_dir") + + if verify_plugin_manifest "$OPENCLAW_PLUGINS_ROOT/$plugin"; then + plugins+=("$plugin") + else + echo "[WARN] Skipping $plugin: invalid or missing plugin manifest" >&2 + fi + done + + printf '%s\n' "${plugins[@]}" | jq -R . | jq -s . +} + +verify_plugin_manifest() { + local dest="$1" + echo "[INFO] Verifying plugin manifest in $dest ..." + if [ ! -f "$dest/openclaw.plugin.json" ]; then + if ! node -e "const p=require('$dest/package.json'); process.exit(p.openclaw ? 0 : 1)" 2>/dev/null; then + echo "[ERROR] $dest has neither openclaw.plugin.json nor openclaw field in package.json!" >&2 + return 1 + fi + fi + echo "[OK] Manifest verified at $dest" +} + +add_plugin() { + local npm_spec="$1" + local plugin_id="$2" + local dest="$OPENCLAW_PLUGINS_ROOT/$plugin_id" + + mkdir -pv "$dest" "$OPENCLAW_PLUGINS_ROOT" "$PNPM_STORE" + + echo "[INFO] Adding $npm_spec ..." + local tarball + tarball=$(npm pack "$npm_spec" --pack-destination /tmp/ 2>/dev/null | tail -1) + + echo "[INFO] Extracting to $dest ..." + tar -xzf "/tmp/$tarball" --strip-components=1 -C "$dest" + rm -f "/tmp/$tarball" + + verify_plugin_manifest "$dest" && echo "[OK] Plugin $plugin_id installed via pnpm" || return 2 +} diff --git a/docker_openclaw/work/start-openclaw.sh b/docker_openclaw/work/start-openclaw.sh index 166d185..432f5b3 100644 --- a/docker_openclaw/work/start-openclaw.sh +++ b/docker_openclaw/work/start-openclaw.sh @@ -1,25 +1,30 @@ -#!/bin/sh +#!/usr/bin/env bash set -eu -export OPENCLAW_HOME="${OPENCLAW_HOME:-/opt/openclaw}" -export OPENCLAW_DIR_STATE="${OPENCLAW_DIR_STATE:-${XDG_CONFIG_HOME:-$OPENCLAW_HOME/data}}" -export OPENCLAW_CONFIG="${OPENCLAW_CONFIG:-$OPENCLAW_DIR_STATE/openclaw.json}" - -export OPENCLAW_HIDE_BANNER=1 bootstrap() { - mkdir -pv "${OPENCLAW_DIR_STATE}" "$(dirname "${OPENCLAW_CONFIG}")" + . /opt/openclaw/script-setup-openclaw.sh + + init_config - local PATH_PLUGIN_INSTALLER="${OPENCLAW_PLUGIN_INSTALLER:-$OPENCLAW_HOME/openclaw-plugin-installer.js}" - local CLAW_EXEC="node ${PATH_PLUGIN_INSTALLER} --config ${OPENCLAW_CONFIG}" + local plugins_json + plugins_json=$(list_downloaded_plugins) + plugins_json=${plugins_json:-"[]"} - $CLAW_EXEC init-config - openclaw config set skills.install.nodeManager pnpm + jq \ + --argjson plugins "$plugins_json" \ + ' + def build_entries: + reduce $plugins[] as $p ({}; .[$p] = {enabled: true}); + .plugins.entries = build_entries + ' "$OPENCLAW_CONFIG" > "${OPENCLAW_CONFIG}.tmp" \ + && mv "${OPENCLAW_CONFIG}.tmp" "$OPENCLAW_CONFIG" - $CLAW_EXEC disable --entry "feishu" - $CLAW_EXEC install --package "@larksuite/openclaw-lark" + echo "[OK] Plugins entries updated" } /opt/utils/script-localize.sh "${PROFILE_LOCALIZE:-default}" -bootstrap +[ ! -f "$OPENCLAW_CONFIG" ] && bootstrap + +echo "Starting openclaw with options: $@" exec openclaw "$@" diff --git a/tool.sh b/tool.sh index 804a990..9fd281c 100644 --- a/tool.sh +++ b/tool.sh @@ -1,5 +1,5 @@ #!/bin/bash -set -xu +set -eux CI_PROJECT_NAME=${CI_PROJECT_NAME:-$GITHUB_REPOSITORY} CI_PROJECT_BRANCH=${GITHUB_HEAD_REF:-"main"} @@ -25,30 +25,30 @@ echo "--------> DOCKER_TAG_SUFFIX=${TAG_SUFFIX}" build_image() { echo "$@" ; IMG=$1; TAG=$2; FILE=$3; shift 3; VER=$(date +%Y.%m%d.%H%M)${TAG_SUFFIX}; WORKDIR="$(dirname $FILE)"; - docker build --compress --force-rm=true -t "${IMG_PREFIX_DST}/${IMG}:${TAG}" -f "$FILE" --build-arg "BASE_NAMESPACE=${IMG_PREFIX_SRC}" "$@" "${WORKDIR}" >&2 - docker tag "${IMG_PREFIX_DST}/${IMG}:${TAG}" "${IMG_PREFIX_DST}/${IMG}:${VER}" >&2 + docker build --compress --force-rm=true -t "${IMG_PREFIX_DST}/${IMG}:${TAG}" -f "$FILE" --build-arg "BASE_NAMESPACE=${IMG_PREFIX_SRC}" "$@" "${WORKDIR}" + docker tag "${IMG_PREFIX_DST}/${IMG}:${TAG}" "${IMG_PREFIX_DST}/${IMG}:${VER}" echo "${IMG_PREFIX_DST}/${IMG}:${TAG}" } build_image_no_tag() { echo "$@" ; IMG=$1; TAG=$2; FILE=$3; shift 3; WORKDIR="$(dirname $FILE)"; - docker build --compress --force-rm=true -t "${IMG_PREFIX_DST}/${IMG}:${TAG}" -f "$FILE" --build-arg "BASE_NAMESPACE=${IMG_PREFIX_SRC}" "$@" "${WORKDIR}" >&2 + docker build --compress --force-rm=true -t "${IMG_PREFIX_DST}/${IMG}:${TAG}" -f "$FILE" --build-arg "BASE_NAMESPACE=${IMG_PREFIX_SRC}" "$@" "${WORKDIR}" echo "${IMG_PREFIX_DST}/${IMG}:${TAG}" } build_image_common() { echo "$@" ; IMG=$1; TAG=$2; FILE=$3; shift 3; VER=$(date +%Y.%m%d.%H%M)${TAG_SUFFIX}; WORKDIR="$(dirname $FILE)"; - docker build --compress --force-rm=true -t "${IMG_PREFIX_DST}/${IMG}:${TAG}" -f "$FILE" --build-arg "BASE_NAMESPACE=${IMG_PREFIX_SRC}" "$@" "${WORKDIR}" >&2 - docker tag "${IMG_PREFIX_DST}/${IMG}:${TAG}" "${IMG_PREFIX_DST}/${IMG}:${VER}" >&2 + docker build --compress --force-rm=true -t "${IMG_PREFIX_DST}/${IMG}:${TAG}" -f "$FILE" --build-arg "BASE_NAMESPACE=${IMG_PREFIX_SRC}" "$@" "${WORKDIR}" + docker tag "${IMG_PREFIX_DST}/${IMG}:${TAG}" "${IMG_PREFIX_DST}/${IMG}:${VER}" echo "${IMG_PREFIX_DST}/${IMG}:${TAG}" } alias_image() { IMG_1=$1; TAG_1=$2; IMG_2=$3; TAG_2=$4; shift 4; VER=$(date +%Y.%m%d.%H%M)${TAG_SUFFIX}; - docker tag "${IMG_PREFIX_DST}/${IMG_1}:${TAG_1}" "${IMG_PREFIX_DST}/${IMG_2}:${TAG_2}" >&2 - docker tag "${IMG_PREFIX_DST}/${IMG_2}:${TAG_2}" "${IMG_PREFIX_DST}/${IMG_2}:${VER}" >&2 + docker tag "${IMG_PREFIX_DST}/${IMG_1}:${TAG_1}" "${IMG_PREFIX_DST}/${IMG_2}:${TAG_2}" + docker tag "${IMG_PREFIX_DST}/${IMG_2}:${TAG_2}" "${IMG_PREFIX_DST}/${IMG_2}:${VER}" } push_image() {