ci(docker): prune old image versions
This commit is contained in:
@@ -131,6 +131,22 @@ jobs:
|
|||||||
docker push "$IMAGE_VERSION_TAG"
|
docker push "$IMAGE_VERSION_TAG"
|
||||||
docker push "$IMAGE_LATEST_TAG"
|
docker push "$IMAGE_LATEST_TAG"
|
||||||
|
|
||||||
|
- name: Prune Gitea registry images
|
||||||
|
env:
|
||||||
|
GITEA_API_BASE_URL: ${{ secrets.GITEA_API_BASE_URL }}
|
||||||
|
GITEA_PACKAGE_TOKEN: ${{ secrets.GITEA_PACKAGE_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
if [ -z "$GITEA_PACKAGE_TOKEN" ]; then
|
||||||
|
echo "Missing secret: GITEA_PACKAGE_TOKEN" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
GITEA_API_BASE_URL="${GITEA_API_BASE_URL:-https://gitea.banlv-ai.com/api/v1}" \
|
||||||
|
GITEA_PACKAGE_KEEP_COUNT=3 \
|
||||||
|
bash scripts/ci/prune-gitea-container-images.sh
|
||||||
|
|
||||||
- name: Deploy image over SSH
|
- name: Deploy image over SSH
|
||||||
env:
|
env:
|
||||||
SSH_HOST: ${{ secrets.SSH_HOST }}
|
SSH_HOST: ${{ secrets.SSH_HOST }}
|
||||||
@@ -201,8 +217,13 @@ jobs:
|
|||||||
| ssh $SSH_OPTS "$SSH_TARGET" \
|
| ssh $SSH_OPTS "$SSH_TARGET" \
|
||||||
"docker login '$REGISTRY_HOST' -u '$REGISTRY_USERNAME' --password-stdin"
|
"docker login '$REGISTRY_HOST' -u '$REGISTRY_USERNAME' --password-stdin"
|
||||||
|
|
||||||
|
SERVER_RETAIN_COUNT="0"
|
||||||
|
if [ "$DEPLOY_ENV" = "prod" ]; then
|
||||||
|
SERVER_RETAIN_COUNT="1"
|
||||||
|
fi
|
||||||
|
|
||||||
ssh $SSH_OPTS "$SSH_TARGET" \
|
ssh $SSH_OPTS "$SSH_TARGET" \
|
||||||
"cd '$DEPLOY_DIR' && chmod +x ./deploy-docker-image.sh && ./deploy-docker-image.sh '$IMAGE_VERSION_TAG' '$NEXT_ENV_FILE' '$HOST_PORT' '$CONTAINER_NAME' '$PROJECT_NAME'"
|
"cd '$DEPLOY_DIR' && chmod +x ./deploy-docker-image.sh && ./deploy-docker-image.sh '$IMAGE_VERSION_TAG' '$NEXT_ENV_FILE' '$HOST_PORT' '$CONTAINER_NAME' '$PROJECT_NAME' '$SERVER_RETAIN_COUNT'"
|
||||||
|
|
||||||
- name: Purge Cloudflare cache
|
- name: Purge Cloudflare cache
|
||||||
env:
|
env:
|
||||||
@@ -230,6 +251,6 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
env:
|
env:
|
||||||
ACTION_JOB_STATUS: ${{ job.status }}
|
ACTION_JOB_STATUS: ${{ job.status }}
|
||||||
FLOW_NOTIFY_BASE_URL: ${{ secrets.FLOW_NOTIFY_BASE_URL }}
|
ACTION_NOTIFY_WEBHOOK_URL: ${{ secrets.ACTION_NOTIFY_WEBHOOK_URL }}
|
||||||
run: |
|
run: |
|
||||||
bash scripts/ci/notify-actions.sh
|
bash scripts/ci/notify-actions.sh
|
||||||
|
|||||||
@@ -23,6 +23,8 @@
|
|||||||
| `REGISTRY_IMAGE` | `gitea.banlv-ai.com/admin/cozsweet-web` | 完整镜像仓库名,不包含 tag |
|
| `REGISTRY_IMAGE` | `gitea.banlv-ai.com/admin/cozsweet-web` | 完整镜像仓库名,不包含 tag |
|
||||||
| `REGISTRY_USERNAME` | `cozsweet-bot` | 镜像仓库用户名 |
|
| `REGISTRY_USERNAME` | `cozsweet-bot` | 镜像仓库用户名 |
|
||||||
| `REGISTRY_PASSWORD` | `***` | 镜像仓库密码或访问令牌 |
|
| `REGISTRY_PASSWORD` | `***` | 镜像仓库密码或访问令牌 |
|
||||||
|
| `GITEA_API_BASE_URL` | `https://gitea.banlv-ai.com/api/v1` | Gitea API 地址,用于清理旧镜像版本 |
|
||||||
|
| `GITEA_PACKAGE_TOKEN` | `***` | 具备 package 删除权限的 Gitea token |
|
||||||
| `TEST_ENV_FILE` | `.env.local` 的完整内容 | 测试环境构建期环境变量 |
|
| `TEST_ENV_FILE` | `.env.local` 的完整内容 | 测试环境构建期环境变量 |
|
||||||
| `PRODUCTION_ENV_FILE` | `.env.production` 的完整内容 | 生产环境构建期环境变量 |
|
| `PRODUCTION_ENV_FILE` | `.env.production` 的完整内容 | 生产环境构建期环境变量 |
|
||||||
|
|
||||||
@@ -106,3 +108,13 @@ gitea.banlv-ai.com/admin/cozsweet-web:prod-<short_sha>
|
|||||||
```
|
```
|
||||||
|
|
||||||
推荐优先使用默认的 `<env>-<short_sha>` 精确 tag,这样发布和回滚都更可控。
|
推荐优先使用默认的 `<env>-<short_sha>` 精确 tag,这样发布和回滚都更可控。
|
||||||
|
|
||||||
|
## 镜像保留策略
|
||||||
|
|
||||||
|
镜像推送成功后,workflow 会调用 Gitea Package API 清理远端 Container Registry:
|
||||||
|
|
||||||
|
- `test-*` 精确 tag 只保留最近 3 个,`test-latest` 不计入也不删除。
|
||||||
|
- `prod-*` 精确 tag 只保留最近 3 个,`prod-latest` 不计入也不删除。
|
||||||
|
- 当前刚推送的 `IMAGE_VERSION_TAG` 永远不会被删除。
|
||||||
|
|
||||||
|
生产环境 SSH 部署成功后,服务器本机只保留当前正在运行的一个 `prod-*` 镜像;测试环境服务器本机暂不限制保留数量。
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ push test/main
|
|||||||
| `REGISTRY_IMAGE` | 镜像仓库,例如 `gitea.banlv-ai.com/admin/cozsweet-web` |
|
| `REGISTRY_IMAGE` | 镜像仓库,例如 `gitea.banlv-ai.com/admin/cozsweet-web` |
|
||||||
| `REGISTRY_USERNAME` | registry 用户名 |
|
| `REGISTRY_USERNAME` | registry 用户名 |
|
||||||
| `REGISTRY_PASSWORD` | registry 密码或 token |
|
| `REGISTRY_PASSWORD` | registry 密码或 token |
|
||||||
|
| `GITEA_API_BASE_URL` | Gitea API 地址,例如 `https://gitea.banlv-ai.com/api/v1` |
|
||||||
|
| `GITEA_PACKAGE_TOKEN` | 具备 package 删除权限的 Gitea token,用于清理旧镜像 tag |
|
||||||
| `TEST_ENV_FILE` | 测试环境 `.env.local` 内容 |
|
| `TEST_ENV_FILE` | 测试环境 `.env.local` 内容 |
|
||||||
| `PRODUCTION_ENV_FILE` | 生产环境 `.env.production` 内容 |
|
| `PRODUCTION_ENV_FILE` | 生产环境 `.env.production` 内容 |
|
||||||
|
|
||||||
@@ -61,6 +63,19 @@ deploy-docker-image.sh
|
|||||||
.env.local 或 .env.production
|
.env.local 或 .env.production
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 镜像保留策略
|
||||||
|
|
||||||
|
Actions 推送镜像后会清理 Gitea Container Registry,同一环境只保留最近 3 个精确版本 tag:
|
||||||
|
|
||||||
|
```text
|
||||||
|
test-<short_sha> 最近 3 个
|
||||||
|
prod-<short_sha> 最近 3 个
|
||||||
|
```
|
||||||
|
|
||||||
|
`test-latest` / `prod-latest` 不计入保留数量,也不会主动删除。
|
||||||
|
|
||||||
|
生产环境部署脚本会在 `docker compose up` 成功后清理服务器本机旧镜像,只保留当前 `prod-<short_sha>` 镜像。测试环境服务器本机镜像暂不限制保留数量。
|
||||||
|
|
||||||
## 本地发布入口
|
## 本地发布入口
|
||||||
|
|
||||||
本地发布脚本现在只负责推送 Gitea 分支,触发 Actions:
|
本地发布脚本现在只负责推送 Gitea 分支,触发 Actions:
|
||||||
|
|||||||
@@ -5,28 +5,6 @@ warn() {
|
|||||||
printf 'Notification warning: %s\n' "$*" >&2
|
printf 'Notification warning: %s\n' "$*" >&2
|
||||||
}
|
}
|
||||||
|
|
||||||
generate_chat_id() {
|
|
||||||
if [ -n "${ACTION_NOTIFY_CHAT_ID:-}" ]; then
|
|
||||||
printf '%s' "$ACTION_NOTIFY_CHAT_ID"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v node >/dev/null 2>&1; then
|
|
||||||
node <<'NODE'
|
|
||||||
const { randomUUID } = require("node:crypto");
|
|
||||||
process.stdout.write(`oc_${randomUUID().replaceAll("-", "")}`);
|
|
||||||
NODE
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v openssl >/dev/null 2>&1; then
|
|
||||||
printf 'oc_%s' "$(openssl rand -hex 16)"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf 'oc_%s%s' "$(date +%s)" "$$"
|
|
||||||
}
|
|
||||||
|
|
||||||
branch_name="${BRANCH_NAME:-${GITHUB_REF_NAME:-}}"
|
branch_name="${BRANCH_NAME:-${GITHUB_REF_NAME:-}}"
|
||||||
if [ -z "$branch_name" ] && [ -n "${GITHUB_REF:-}" ]; then
|
if [ -z "$branch_name" ] && [ -n "${GITHUB_REF:-}" ]; then
|
||||||
branch_name="${GITHUB_REF#refs/heads/}"
|
branch_name="${GITHUB_REF#refs/heads/}"
|
||||||
@@ -43,9 +21,11 @@ if [ -z "$short_sha" ]; then
|
|||||||
short_sha="unknown"
|
short_sha="unknown"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
base_url="${FLOW_NOTIFY_BASE_URL:-https://flow.banlv-ai.com}"
|
notify_url="${ACTION_NOTIFY_WEBHOOK_URL:-}"
|
||||||
base_url="${base_url%/}"
|
if [ -z "$notify_url" ]; then
|
||||||
chat_id="$(generate_chat_id)"
|
warn "ACTION_NOTIFY_WEBHOOK_URL is not configured, skip notification."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
deploy_env="${DEPLOY_ENV:-unknown}"
|
deploy_env="${DEPLOY_ENV:-unknown}"
|
||||||
image_tag="${IMAGE_VERSION_TAG:-unknown}"
|
image_tag="${IMAGE_VERSION_TAG:-unknown}"
|
||||||
@@ -84,11 +64,6 @@ process.stdout.write(JSON.stringify(payload));
|
|||||||
NODE
|
NODE
|
||||||
)"
|
)"
|
||||||
|
|
||||||
encoded_chat_id="$(
|
|
||||||
node -e 'process.stdout.write(encodeURIComponent(process.argv[1] || ""))' "$chat_id"
|
|
||||||
)"
|
|
||||||
notify_url="$base_url/webhooks/action-notify?chat_id=$encoded_chat_id"
|
|
||||||
|
|
||||||
if [ "${FLOW_NOTIFY_DRY_RUN:-}" = "1" ]; then
|
if [ "${FLOW_NOTIFY_DRY_RUN:-}" = "1" ]; then
|
||||||
printf 'Action notification dry run URL: %s\n' "$notify_url"
|
printf 'Action notification dry run URL: %s\n' "$notify_url"
|
||||||
printf 'Action notification dry run payload: %s\n' "$payload"
|
printf 'Action notification dry run payload: %s\n' "$payload"
|
||||||
@@ -99,8 +74,8 @@ if ! curl -fsS \
|
|||||||
-X POST "$notify_url" \
|
-X POST "$notify_url" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "$payload" >/dev/null; then
|
-d "$payload" >/dev/null; then
|
||||||
warn "failed to send action notification for $chat_id"
|
warn "failed to send action notification."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Sent action notification for $chat_id."
|
echo "Sent action notification."
|
||||||
|
|||||||
Executable
+162
@@ -0,0 +1,162 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
KEEP_COUNT="${GITEA_PACKAGE_KEEP_COUNT:-3}"
|
||||||
|
API_BASE_URL="${GITEA_API_BASE_URL:-https://gitea.banlv-ai.com/api/v1}"
|
||||||
|
TOKEN="${GITEA_PACKAGE_TOKEN:-}"
|
||||||
|
REGISTRY_IMAGE="${REGISTRY_IMAGE:-${IMAGE:-}}"
|
||||||
|
DEPLOY_ENV="${DEPLOY_ENV:-}"
|
||||||
|
CURRENT_IMAGE_TAG="${IMAGE_VERSION_TAG:-}"
|
||||||
|
|
||||||
|
if [ -z "$TOKEN" ] && [ -z "${GITEA_PACKAGE_VERSIONS_FILE:-}" ]; then
|
||||||
|
echo "Missing GITEA_PACKAGE_TOKEN" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$REGISTRY_IMAGE" ] || [ -z "$DEPLOY_ENV" ]; then
|
||||||
|
echo "Missing REGISTRY_IMAGE/IMAGE or DEPLOY_ENV" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$DEPLOY_ENV" in
|
||||||
|
test|prod) ;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported DEPLOY_ENV for package pruning: $DEPLOY_ENV" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if ! command -v node >/dev/null 2>&1; then
|
||||||
|
echo "node is required for package pruning" >&2
|
||||||
|
exit 127
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v curl >/dev/null 2>&1; then
|
||||||
|
echo "curl is required for package pruning" >&2
|
||||||
|
exit 127
|
||||||
|
fi
|
||||||
|
|
||||||
|
image_without_tag="${REGISTRY_IMAGE%%:*}"
|
||||||
|
path_without_registry="${image_without_tag#*/}"
|
||||||
|
owner="${path_without_registry%%/*}"
|
||||||
|
package="${path_without_registry#*/}"
|
||||||
|
|
||||||
|
if [ "$owner" = "$path_without_registry" ] || [ -z "$owner" ] || [ -z "$package" ]; then
|
||||||
|
echo "Could not parse owner/package from REGISTRY_IMAGE: $REGISTRY_IMAGE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
current_version="${CURRENT_IMAGE_TAG##*:}"
|
||||||
|
version_prefix="${DEPLOY_ENV}-"
|
||||||
|
latest_version="${DEPLOY_ENV}-latest"
|
||||||
|
api_base="${API_BASE_URL%/}"
|
||||||
|
|
||||||
|
echo "=== prune Gitea container package versions ==="
|
||||||
|
echo "owner: $owner"
|
||||||
|
echo "package: $package"
|
||||||
|
echo "environment: $DEPLOY_ENV"
|
||||||
|
echo "keep count: $KEEP_COUNT"
|
||||||
|
|
||||||
|
versions_json="$(mktemp)"
|
||||||
|
delete_list="$(mktemp)"
|
||||||
|
cleanup() {
|
||||||
|
rm -f "$versions_json" "$delete_list"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
if [ -n "${GITEA_PACKAGE_VERSIONS_FILE:-}" ]; then
|
||||||
|
cp "$GITEA_PACKAGE_VERSIONS_FILE" "$versions_json"
|
||||||
|
else
|
||||||
|
curl -fsS \
|
||||||
|
-H "Authorization: token $TOKEN" \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
"$api_base/packages/$owner/container/$package" \
|
||||||
|
-o "$versions_json"
|
||||||
|
fi
|
||||||
|
|
||||||
|
KEEP_COUNT="$KEEP_COUNT" \
|
||||||
|
VERSION_PREFIX="$version_prefix" \
|
||||||
|
LATEST_VERSION="$latest_version" \
|
||||||
|
CURRENT_VERSION="$current_version" \
|
||||||
|
VERSIONS_JSON_FILE="$versions_json" \
|
||||||
|
node <<'NODE' > "$delete_list"
|
||||||
|
const fs = require("node:fs");
|
||||||
|
|
||||||
|
const file = process.env.VERSIONS_JSON_FILE;
|
||||||
|
const keepCount = Number.parseInt(process.env.KEEP_COUNT || "3", 10);
|
||||||
|
const prefix = process.env.VERSION_PREFIX || "";
|
||||||
|
const latest = process.env.LATEST_VERSION || "";
|
||||||
|
const current = process.env.CURRENT_VERSION || "";
|
||||||
|
const raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
||||||
|
const list = Array.isArray(raw) ? raw : Array.isArray(raw.versions) ? raw.versions : [];
|
||||||
|
|
||||||
|
function versionOf(item) {
|
||||||
|
return String(item.version || item.name || item.tag_name || "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createdAtOf(item) {
|
||||||
|
return String(
|
||||||
|
item.created_at ||
|
||||||
|
item.created ||
|
||||||
|
item.createdAt ||
|
||||||
|
item.published_at ||
|
||||||
|
item.updated_at ||
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidates = list
|
||||||
|
.map((item) => ({
|
||||||
|
version: versionOf(item),
|
||||||
|
createdAt: createdAtOf(item),
|
||||||
|
}))
|
||||||
|
.filter(({ version }) => {
|
||||||
|
if (!version.startsWith(prefix)) return false;
|
||||||
|
if (version === latest) return false;
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
const bTime = Date.parse(b.createdAt);
|
||||||
|
const aTime = Date.parse(a.createdAt);
|
||||||
|
if (Number.isFinite(bTime) && Number.isFinite(aTime) && bTime !== aTime) {
|
||||||
|
return bTime - aTime;
|
||||||
|
}
|
||||||
|
return b.version.localeCompare(a.version);
|
||||||
|
});
|
||||||
|
|
||||||
|
const kept = new Set(candidates.slice(0, Math.max(0, keepCount)).map((item) => item.version));
|
||||||
|
const toDelete = candidates
|
||||||
|
.slice(Math.max(0, keepCount))
|
||||||
|
.map((item) => item.version)
|
||||||
|
.filter((version) => version !== current && !kept.has(version));
|
||||||
|
process.stdout.write(toDelete.join("\n"));
|
||||||
|
if (toDelete.length > 0) process.stdout.write("\n");
|
||||||
|
NODE
|
||||||
|
|
||||||
|
if [ ! -s "$delete_list" ]; then
|
||||||
|
echo "No old $DEPLOY_ENV package versions to delete."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Package versions selected for deletion:"
|
||||||
|
sed 's/^/ - /' "$delete_list"
|
||||||
|
|
||||||
|
if [ "${PRUNE_DRY_RUN:-}" = "1" ]; then
|
||||||
|
echo "Dry run enabled, skip DELETE requests."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
while IFS= read -r version; do
|
||||||
|
[ -n "$version" ] || continue
|
||||||
|
encoded_owner="$(node -e 'process.stdout.write(encodeURIComponent(process.argv[1] || ""))' "$owner")"
|
||||||
|
encoded_package="$(node -e 'process.stdout.write(encodeURIComponent(process.argv[1] || ""))' "$package")"
|
||||||
|
encoded_version="$(node -e 'process.stdout.write(encodeURIComponent(process.argv[1] || ""))' "$version")"
|
||||||
|
curl -fsS \
|
||||||
|
-X DELETE \
|
||||||
|
-H "Authorization: token $TOKEN" \
|
||||||
|
"$api_base/packages/$encoded_owner/container/$encoded_package/$encoded_version" \
|
||||||
|
>/dev/null
|
||||||
|
echo "Deleted package version: $version"
|
||||||
|
done < "$delete_list"
|
||||||
|
|
||||||
|
echo "=== package prune done ==="
|
||||||
@@ -7,9 +7,10 @@ ENV_FILE="${2:-}"
|
|||||||
HOST_PORT="${3:-}"
|
HOST_PORT="${3:-}"
|
||||||
CONTAINER_NAME="${4:-}"
|
CONTAINER_NAME="${4:-}"
|
||||||
PROJECT_NAME="${5:-}"
|
PROJECT_NAME="${5:-}"
|
||||||
|
SERVER_RETAIN_COUNT="${6:-0}"
|
||||||
|
|
||||||
if [ -z "$IMAGE" ] || [ -z "$ENV_FILE" ] || [ -z "$HOST_PORT" ] || [ -z "$CONTAINER_NAME" ] || [ -z "$PROJECT_NAME" ]; then
|
if [ -z "$IMAGE" ] || [ -z "$ENV_FILE" ] || [ -z "$HOST_PORT" ] || [ -z "$CONTAINER_NAME" ] || [ -z "$PROJECT_NAME" ]; then
|
||||||
echo "Usage: $0 <image> <env-file> <host-port> <container-name> <compose-project-name>" >&2
|
echo "Usage: $0 <image> <env-file> <host-port> <container-name> <compose-project-name> [server-retain-count]" >&2
|
||||||
exit 64
|
exit 64
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -50,4 +51,38 @@ echo "=== compose project: $COMPOSE_PROJECT_NAME ==="
|
|||||||
|
|
||||||
docker image prune -f
|
docker image prune -f
|
||||||
|
|
||||||
|
prune_project_images() {
|
||||||
|
local retain_count="$1"
|
||||||
|
if ! [[ "$retain_count" =~ ^[0-9]+$ ]] || [ "$retain_count" -le 0 ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local image_repo="${IMAGE%%:*}"
|
||||||
|
local image_tag="${IMAGE##*:}"
|
||||||
|
local tag_prefix="${image_tag%%-*}-"
|
||||||
|
|
||||||
|
if [ -z "$image_repo" ] || [ "$image_repo" = "$IMAGE" ] || [ -z "$tag_prefix" ]; then
|
||||||
|
echo "Skip project image prune: could not parse image repo/tag from $IMAGE"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== prune local project images: keep $retain_count for $tag_prefix ==="
|
||||||
|
|
||||||
|
docker image ls "$image_repo" \
|
||||||
|
--format '{{.Repository}}:{{.Tag}} {{.CreatedAt}}' \
|
||||||
|
| awk -v prefix="$tag_prefix" '$1 ~ ":" prefix && $1 !~ /-latest$/ { print }' \
|
||||||
|
| sort -k2,3r \
|
||||||
|
| awk -v keep="$retain_count" 'NR > keep { print $1 }' \
|
||||||
|
| while IFS= read -r old_image; do
|
||||||
|
[ -n "$old_image" ] || continue
|
||||||
|
if [ "$old_image" = "$IMAGE" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "Removing local image: $old_image"
|
||||||
|
docker image rm "$old_image" || true
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
prune_project_images "$SERVER_RETAIN_COUNT"
|
||||||
|
|
||||||
echo "=== deploy done ==="
|
echo "=== deploy done ==="
|
||||||
|
|||||||
Reference in New Issue
Block a user