From fb66e9bb290275abd8c8f48f97a8086f53cb2ac4 Mon Sep 17 00:00:00 2001 From: Nikita Korolev Date: Tue, 9 Jun 2026 13:34:28 +0300 Subject: [PATCH 1/3] chore(ci): clean up custom release tags in dev registry werf cleanup keep policies only apply to werf-managed images, so the pr*, release-*, and v*-rc.* tags published to the /release repository via crane copy were never removed. Add a dedicated crane-based cleanup script for the /release repository and wire it into the dev registry cleanup workflow with a dry-run input and full git history checkout. Signed-off-by: Nikita Korolev --- .../scripts/bash/registry-module-cleanup.sh | 114 ++++++++++++++++++ .github/workflows/dev_registry-cleanup.yml | 23 +++- 2 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 .github/scripts/bash/registry-module-cleanup.sh diff --git a/.github/scripts/bash/registry-module-cleanup.sh b/.github/scripts/bash/registry-module-cleanup.sh new file mode 100644 index 0000000000..4d61164793 --- /dev/null +++ b/.github/scripts/bash/registry-module-cleanup.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash + +# Copyright 2026 Flant JSC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -Eeuo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=.github/scripts/bash/e2e/common.sh +source "${SCRIPT_DIR}/e2e/common.sh" + +require_env MODULES_MODULE_SOURCE +require_env MODULES_MODULE_NAME + +readonly PR_TAG_TTL_DAYS="${REGISTRY_CLEANUP_PR_TAG_TTL_DAYS:-14}" +readonly RC_TAG_TTL_DAYS="${REGISTRY_CLEANUP_RC_TAG_TTL_DAYS:-60}" +readonly DRY_RUN="${REGISTRY_CLEANUP_DRY_RUN:-false}" +# MODULES_MODULE_SOURCE and MODULES_MODULE_NAME come from the workflow env block +# and are validated by require_env above. +# shellcheck disable=SC2154 +readonly RELEASE_REPO="${MODULES_MODULE_SOURCE}/${MODULES_MODULE_NAME}/release" +readonly SECONDS_PER_DAY=$((24 * 60 * 60)) + +now_epoch="$(date +%s)" +readonly pr_threshold_epoch=$((now_epoch - PR_TAG_TTL_DAYS * SECONDS_PER_DAY)) +readonly rc_threshold_epoch=$((now_epoch - RC_TAG_TTL_DAYS * SECONDS_PER_DAY)) + +log() { + echo "[INFO] $*" +} + +tag_created_epoch() { + local tag="$1" + + crane config "${RELEASE_REPO}:${tag}" \ + | jq -r '.created | split(".")[0] + "Z" | fromdateiso8601' +} + +# Returns 0 if the tag is expired and should be deleted. +# Protected tags (release channels, stable vX.Y.Z, main) never match. +is_expired_tag() { + local tag="$1" + local created_epoch="$2" + + case "${tag}" in + pr[0-9]* | release-*) + [ "${created_epoch}" -lt "${pr_threshold_epoch}" ] + ;; + v[0-9]*.[0-9]*.[0-9]*-rc.[0-9]*) + [ "${created_epoch}" -lt "${rc_threshold_epoch}" ] + ;; + *) + return 1 + ;; + esac +} + +# crane delete removes the manifest by the digest the tag points to. Tags that +# share a digest with the deleted one are untagged too, so only expired and +# unprotected tags reach this function. +delete_tag() { + local tag="$1" + + if [ "${DRY_RUN}" = "true" ]; then + log "[dry-run] would delete ${RELEASE_REPO}:${tag}" + return + fi + + log "deleting ${RELEASE_REPO}:${tag}" + crane delete "${RELEASE_REPO}:${tag}" +} + +cleanup_repo() { + local tag created_epoch + local deleted=0 kept=0 + + while read -r tag; do + [ -z "${tag}" ] && continue + + created_epoch="$(tag_created_epoch "${tag}")" + if [ -z "${created_epoch}" ]; then + log "skip ${tag}: cannot resolve creation time" + kept=$((kept + 1)) + continue + fi + + if is_expired_tag "${tag}" "${created_epoch}"; then + delete_tag "${tag}" + deleted=$((deleted + 1)) + else + kept=$((kept + 1)) + fi + done < <(crane ls "${RELEASE_REPO}") + + log "done: ${deleted} deleted, ${kept} kept" +} + +log "cleaning custom release tags in ${RELEASE_REPO}" +log "dry run: ${DRY_RUN}" +log "pr/release branch tag TTL: ${PR_TAG_TTL_DAYS} days" +log "release candidate tag TTL: ${RC_TAG_TTL_DAYS} days" + +cleanup_repo diff --git a/.github/workflows/dev_registry-cleanup.yml b/.github/workflows/dev_registry-cleanup.yml index 1b73ada53f..5855ec16f5 100644 --- a/.github/workflows/dev_registry-cleanup.yml +++ b/.github/workflows/dev_registry-cleanup.yml @@ -25,6 +25,15 @@ env: on: workflow_dispatch: + inputs: + dry_run: + description: "Show custom release tag cleanup actions without deleting tags" + required: true + default: "true" + type: choice + options: + - "true" + - "false" schedule: - cron: "12 0 * * 6" @@ -38,7 +47,11 @@ jobs: name: Run cleanup steps: - uses: actions/checkout@v4 - - uses: deckhouse/modules-actions/setup@v2 + with: + fetch-depth: 0 + fetch-tags: true + + - uses: deckhouse/modules-actions/setup@v4 with: registry: ${{ vars.DEV_REGISTRY }} registry_login: ${{ vars.DEV_MODULES_REGISTRY_LOGIN }} @@ -47,5 +60,11 @@ jobs: - name: Cleanup run: | werf cleanup \ - --repo ${MODULES_MODULE_SOURCE}/${MODULES_MODULE_NAME} \ + --repo "${MODULES_MODULE_SOURCE}/${MODULES_MODULE_NAME}" \ --without-kube=true --config werf_cleanup.yaml + + - name: Cleanup custom release tags + env: + REGISTRY_CLEANUP_DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || 'false' }} + run: | + bash .github/scripts/bash/registry-module-cleanup.sh From 11ede0c510b2e707fbfbf6599d75bbac839ecdba Mon Sep 17 00:00:00 2001 From: Nikita Korolev Date: Tue, 9 Jun 2026 15:34:11 +0300 Subject: [PATCH 2/3] fix Signed-off-by: Nikita Korolev --- .github/scripts/bash/registry-module-cleanup.sh | 4 ++-- .github/workflows/dev_registry-cleanup.yml | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/scripts/bash/registry-module-cleanup.sh b/.github/scripts/bash/registry-module-cleanup.sh index 4d61164793..0cdda1ab34 100644 --- a/.github/scripts/bash/registry-module-cleanup.sh +++ b/.github/scripts/bash/registry-module-cleanup.sh @@ -44,7 +44,7 @@ tag_created_epoch() { local tag="$1" crane config "${RELEASE_REPO}:${tag}" \ - | jq -r '.created | split(".")[0] + "Z" | fromdateiso8601' + | jq -r '.created | sub("\\.[0-9]+";"") | sub("Z?$";"Z") | fromdateiso8601' } # Returns 0 if the tag is expired and should be deleted. @@ -88,7 +88,7 @@ cleanup_repo() { while read -r tag; do [ -z "${tag}" ] && continue - created_epoch="$(tag_created_epoch "${tag}")" + created_epoch="$(tag_created_epoch "${tag}" 2>/dev/null)" || created_epoch="" if [ -z "${created_epoch}" ]; then log "skip ${tag}: cannot resolve creation time" kept=$((kept + 1)) diff --git a/.github/workflows/dev_registry-cleanup.yml b/.github/workflows/dev_registry-cleanup.yml index 5855ec16f5..cc71c6e82c 100644 --- a/.github/workflows/dev_registry-cleanup.yml +++ b/.github/workflows/dev_registry-cleanup.yml @@ -21,7 +21,6 @@ env: MODULES_MODULE_SOURCE: ${{ vars.DEV_MODULE_SOURCE }} MODULES_REGISTRY_LOGIN: ${{ vars.DEV_MODULES_REGISTRY_LOGIN }} MODULES_REGISTRY_PASSWORD: ${{ secrets.DEV_MODULES_REGISTRY_PASSWORD }} - WERF_DRY_RUN: "false" on: workflow_dispatch: @@ -58,6 +57,8 @@ jobs: registry_password: ${{ secrets.DEV_MODULES_REGISTRY_PASSWORD }} - name: Cleanup + env: + WERF_DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || 'false' }} run: | werf cleanup \ --repo "${MODULES_MODULE_SOURCE}/${MODULES_MODULE_NAME}" \ From 1c7faec739148930b3dac003a86a2d4b49a7be36 Mon Sep 17 00:00:00 2001 From: Nikita Korolev Date: Tue, 9 Jun 2026 19:35:35 +0300 Subject: [PATCH 3/3] resolve comments Signed-off-by: Nikita Korolev --- .../scripts/bash/registry-module-cleanup.sh | 34 +++++++++++-------- .github/workflows/dev_registry-cleanup.yml | 2 +- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/.github/scripts/bash/registry-module-cleanup.sh b/.github/scripts/bash/registry-module-cleanup.sh index 0cdda1ab34..95103fbaf2 100644 --- a/.github/scripts/bash/registry-module-cleanup.sh +++ b/.github/scripts/bash/registry-module-cleanup.sh @@ -53,17 +53,13 @@ is_expired_tag() { local tag="$1" local created_epoch="$2" - case "${tag}" in - pr[0-9]* | release-*) - [ "${created_epoch}" -lt "${pr_threshold_epoch}" ] - ;; - v[0-9]*.[0-9]*.[0-9]*-rc.[0-9]*) - [ "${created_epoch}" -lt "${rc_threshold_epoch}" ] - ;; - *) - return 1 - ;; - esac + if [[ "${tag}" =~ ^pr[0-9]+$ || "${tag}" =~ ^release-[0-9]+\.[0-9]+ ]]; then + [ "${created_epoch}" -lt "${pr_threshold_epoch}" ] + elif [[ "${tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then + [ "${created_epoch}" -lt "${rc_threshold_epoch}" ] + else + return 1 + fi } # crane delete removes the manifest by the digest the tag points to. Tags that @@ -83,7 +79,7 @@ delete_tag() { cleanup_repo() { local tag created_epoch - local deleted=0 kept=0 + local deleted=0 kept=0 failed=0 while read -r tag; do [ -z "${tag}" ] && continue @@ -96,14 +92,22 @@ cleanup_repo() { fi if is_expired_tag "${tag}" "${created_epoch}"; then - delete_tag "${tag}" - deleted=$((deleted + 1)) + if delete_tag "${tag}"; then + deleted=$((deleted + 1)) + else + log "WARN: failed to delete ${RELEASE_REPO}:${tag}" + failed=$((failed + 1)) + fi else kept=$((kept + 1)) fi done < <(crane ls "${RELEASE_REPO}") - log "done: ${deleted} deleted, ${kept} kept" + log "done: ${deleted} deleted, ${kept} kept, ${failed} failed" + + if [ "${failed}" -gt 0 ]; then + return 1 + fi } log "cleaning custom release tags in ${RELEASE_REPO}" diff --git a/.github/workflows/dev_registry-cleanup.yml b/.github/workflows/dev_registry-cleanup.yml index cc71c6e82c..6ee1d663d5 100644 --- a/.github/workflows/dev_registry-cleanup.yml +++ b/.github/workflows/dev_registry-cleanup.yml @@ -41,7 +41,7 @@ defaults: shell: bash jobs: - lint: + cleanup: runs-on: [self-hosted, regular] name: Run cleanup steps: