feat: support package name filtering and refactor cleanup flow

This commit is contained in:
2026-05-15 02:50:38 +00:00
parent 3305b57ac0
commit 528f8c75b3
5 changed files with 403 additions and 107 deletions
+160 -106
View File
@@ -10,7 +10,12 @@ fail() {
exit 1
}
declare -A TARGET_PACKAGES=()
trim() {
local value="$1"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
printf '%s' "$value"
}
resolve_token() {
log "Trying token from RUNNER_TOKEN"
@@ -24,7 +29,7 @@ resolve_token() {
return 1
}
resolve_keep_versions() {
resolve_keep_count() {
local raw_value="${INPUT_KEEP_COUNT:-2}"
if [[ -z "${raw_value}" ]]; then
@@ -39,50 +44,68 @@ resolve_keep_versions() {
}
resolve_package_names() {
# Normalize PACKAGE_NAMES into a unique, newline-separated list.
local raw_value="${INPUT_PACKAGE_NAMES:-}"
local normalized package_name
local -a package_name_list
local normalized token
local -A seen=()
local -a package_names=()
if [[ -z "${raw_value}" ]]; then
if [[ -z "$(trim "${raw_value}")" ]]; then
fail "Missing PACKAGE_NAMES"
fi
normalized="${raw_value//$'\n'/,}"
IFS=',' read -r -a tokens <<< "${normalized}"
IFS=',' read -r -a package_name_list <<< "${normalized}"
for package_name in "${package_name_list[@]}"; do
package_name="${package_name#"${package_name%%[![:space:]]*}"}"
package_name="${package_name%"${package_name##*[![:space:]]}"}"
[[ -n "${package_name}" ]] || continue
TARGET_PACKAGES["${package_name}"]=1
for token in "${tokens[@]}"; do
token="$(trim "${token}")"
[[ -n "${token}" ]] || continue
if [[ -z "${seen["${token}"]+x}" ]]; then
seen["${token}"]=1
package_names+=("${token}")
fi
done
if (( ${#TARGET_PACKAGES[@]} == 0 )); then
if (( ${#package_names[@]} == 0 )); then
fail "Missing PACKAGE_NAMES"
fi
printf '%s\n' "${package_names[@]}"
}
init_repo_context() {
local repository="${GITEA_REPOSITORY:-}"
parse_repo_context() {
# Convert the repository slug into owner/repo parts.
local repository="$1"
local owner repo
repository="$(trim "${repository}")"
if [[ -z "${repository}" || "${repository}" != */* ]]; then
fail "Invalid GITEA_REPOSITORY: ${repository:-<empty>}"
fi
REPO_OWNER="${repository%%/*}"
REPO_NAME="${repository#*/}"
owner="${repository%%/*}"
repo="${repository#*/}"
if [[ -z "${owner}" || -z "${repo}" || "${repo}" == */* ]]; then
fail "Invalid GITEA_REPOSITORY: ${repository}"
fi
printf '%s\t%s\n' "${owner}" "${repo}"
}
api_request() {
# Perform one HTTP request and return status metadata as TSV.
# stdout format: http_code<TAB>status_text<TAB>request_id
local method="$1"
local path="$2"
local url body_file headers_file
local body_file="$3"
local headers_file="$4"
local url http_code status_text request_id status_line
url="${GITEA_SERVER_URL%/}${path}"
body_file="$(mktemp)"
headers_file="$(mktemp)"
if ! API_HTTP_CODE="$(
if ! http_code="$(
curl -sS \
-H "Accept: application/json" \
-H "Authorization: token ${RESOLVED_GITEA_TOKEN}" \
@@ -92,50 +115,55 @@ api_request() {
-w '%{http_code}' \
"${url}"
)"; then
rm -f "${body_file}" "${headers_file}"
fail "Request failed: ${method} ${path}"
fi
API_RESPONSE_BODY="$(cat "${body_file}")"
API_RESPONSE_HEADERS="$(cat "${headers_file}")"
API_STATUS_TEXT="$(head -n 1 "${headers_file}" | tr -d '\r' | cut -d' ' -f2-)"
API_REQUEST_ID="$(
grep -iE '^(x-gitea-request-id|x-request-id):' "${headers_file}" | tail -n 1 | cut -d':' -f2- | tr -d '\r' | sed 's/^ *//'
)" || true
rm -f "${body_file}" "${headers_file}"
status_line="$(head -n 1 "${headers_file}" | tr -d '\r')"
status_text="$(printf '%s' "${status_line}" | cut -d' ' -f2-)"
request_id="$(
awk -F': *' 'tolower($1)=="x-gitea-request-id" || tolower($1)=="x-request-id" {value=$2} END {print value}' "${headers_file}" | tr -d '\r'
)"
if [[ -n "${API_REQUEST_ID}" ]]; then
log "${method} ${path} -> ${API_STATUS_TEXT} request_id=${API_REQUEST_ID}"
if [[ -n "${request_id}" ]]; then
log "${method} ${path} -> ${status_text} request_id=${request_id}"
else
log "${method} ${path} -> ${API_STATUS_TEXT}"
log "${method} ${path} -> ${status_text}"
fi
[[ "${API_HTTP_CODE}" =~ ^2 ]]
printf '%s\t%s\t%s\n' "${http_code}" "${status_text}" "${request_id}"
}
fetch_all_pages() {
local base_path="$1"
fetch_package_versions() {
# Fetch and aggregate all package versions for a single package name.
# stdout: JSON array of version objects sorted by page order, later sorted by created_at by the caller.
local owner="$1"
local package_name="$2"
local page=1
local limit=100
local aggregate_file page_file tmp_file page_path page_length
local aggregate_file page_file headers_file meta http_code status_text request_id page_length path
aggregate_file="$(mktemp)"
printf '[]' > "${aggregate_file}"
while :; do
page_path="${base_path}"
if [[ "${page_path}" == *\?* ]]; then
page_path="${page_path}&page=${page}&limit=${limit}"
else
page_path="${page_path}?page=${page}&limit=${limit}"
fi
if ! api_request GET "${page_path}"; then
rm -f "${aggregate_file}"
fail "Unexpected response for ${page_path}"
fi
path="/api/v1/packages/${owner}/nuget/${package_name}?page=${page}&limit=${limit}"
page_file="$(mktemp)"
printf '%s' "${API_RESPONSE_BODY}" > "${page_file}"
headers_file="$(mktemp)"
meta="$(api_request GET "${path}" "${page_file}" "${headers_file}")"
IFS=$'\t' read -r http_code status_text request_id <<< "${meta}"
rm -f "${headers_file}"
if [[ "${http_code}" == "404" ]]; then
rm -f "${page_file}" "${aggregate_file}"
printf '[]'
return 0
fi
if [[ ! "${http_code}" =~ ^2 ]]; then
rm -f "${page_file}" "${aggregate_file}"
fail "Unexpected response for package ${package_name}: ${status_text}"
fi
page_length="$(jq 'length' "${page_file}")"
tmp_file="$(mktemp)"
@@ -155,89 +183,110 @@ fetch_all_pages() {
}
collect_package_candidates() {
local packages_json group_json package_name total_versions candidates_json
# Build the delete candidate file for the requested package names.
# stdout: package_count<TAB>total_version_count<TAB>kept_count<TAB>candidate_count
local owner="$1"
local keep_count="$2"
local candidate_file="$3"
shift 3
local -a package_names=("$@")
local package_name versions_json total_versions candidates_json
local package_count=0
local total_version_count=0
local kept_count=0
local candidate_count=0
packages_json="$(
fetch_all_pages "/api/v1/packages/${REPO_OWNER}?type=nuget"
)"
: > "${candidate_file}"
if [[ "$(jq 'length' <<<"${packages_json}")" -eq 0 ]]; then
log "No nuget packages found for owner ${REPO_OWNER}"
return 0
fi
for package_name in "${package_names[@]}"; do
versions_json="$(fetch_package_versions "${owner}" "${package_name}")"
while IFS= read -r group_json; do
package_name="$(jq -r '.[0].name' <<<"${group_json}")"
if [[ -z "${TARGET_PACKAGES["$package_name"]+x}" ]]; then
if [[ "$(jq 'length' <<<"${versions_json}")" -eq 0 ]]; then
log "No versions found for package ${package_name}"
continue
fi
total_versions="$(jq 'length' <<<"${group_json}")"
package_count=$((package_count + 1))
total_versions="$(jq 'length' <<<"${versions_json}")"
total_version_count=$((total_version_count + total_versions))
PACKAGE_COUNT=$((PACKAGE_COUNT + 1))
TOTAL_VERSION_COUNT=$((TOTAL_VERSION_COUNT + total_versions))
log "Package ${package_name}: total_versions=${total_versions} keep_count=${keep_versions}"
log "Package ${package_name}: total_versions=${total_versions} keep_count=${keep_count}"
log "Package ${package_name} versions (oldest -> newest):"
while IFS=$'\t' read -r version created_at; do
[[ -z "${version}" ]] && continue
log " - ${version} (${created_at})"
done < <(jq -r 'sort_by(.created_at)[] | [.version, .created_at] | @tsv' <<<"${group_json}")
done < <(jq -r 'sort_by(.created_at)[] | [.version, .created_at] | @tsv' <<<"${versions_json}")
if (( total_versions <= keep_versions )); then
if (( total_versions <= keep_count )); then
log " keep all ${total_versions} versions"
KEPT_COUNT=$((KEPT_COUNT + total_versions))
kept_count=$((kept_count + total_versions))
continue
fi
KEPT_COUNT=$((KEPT_COUNT + keep_versions))
kept_count=$((kept_count + keep_count))
candidates_json="$(
jq -c --argjson keep "${keep_versions}" \
'sort_by(.created_at) | .[0:(length - $keep)]' <<<"${group_json}"
jq -c --argjson keep "${keep_count}" \
'sort_by(.created_at) | .[0:(length - $keep)]' <<<"${versions_json}"
)"
while IFS=$'\t' read -r name version created_at; do
[[ -z "${name}" ]] && continue
log "Candidate to delete: package ${name} version ${version} (created: ${created_at})"
printf '%s\t%s\t%s\n' "${name}" "${version}" "${created_at}" >> "${CANDIDATES_FILE}"
CANDIDATE_COUNT=$((CANDIDATE_COUNT + 1))
while IFS=$'\t' read -r package version created_at; do
[[ -z "${package}" ]] && continue
log "Candidate to delete: package ${package} version ${version} (created: ${created_at})"
printf '%s\t%s\t%s\n' "${package}" "${version}" "${created_at}" >> "${candidate_file}"
candidate_count=$((candidate_count + 1))
done < <(jq -r '.[] | [.name, .version, .created_at] | @tsv' <<<"${candidates_json}")
done < <(jq -c 'group_by(.name)[]' <<<"${packages_json}")
done
printf '%s\t%s\t%s\t%s\n' "${package_count}" "${total_version_count}" "${kept_count}" "${candidate_count}"
}
process_candidates() {
local name version created_at
# Delete each queued version and summarize the result.
local owner="$1"
local candidate_file="$2"
local package_count="$3"
local total_version_count="$4"
local kept_count="$5"
local candidate_count="$6"
local deleted_count=0
local error_count=0
local package_name version created_at
local body_file headers_file meta http_code status_text request_id
if [[ ! -s "${CANDIDATES_FILE}" ]]; then
if [[ ! -s "${candidate_file}" ]]; then
log "No delete candidates found"
log "Summary: packages=${PACKAGE_COUNT} versions=${TOTAL_VERSION_COUNT} kept=${KEPT_COUNT} candidates=0 deleted=0 errors=0"
log "Summary: packages=${package_count} versions=${total_version_count} kept=${kept_count} candidates=0 deleted=0 errors=0"
return 0
fi
while IFS=$'\t' read -r name version created_at; do
[[ -z "${name}" ]] && continue
while IFS=$'\t' read -r package_name version created_at; do
[[ -z "${package_name}" ]] && continue
if api_request DELETE "/api/v1/packages/${REPO_OWNER}/nuget/${name}/${version}"; then
log "Deleted package ${name} version ${version} -> ${API_STATUS_TEXT}"
body_file="$(mktemp)"
headers_file="$(mktemp)"
meta="$(api_request DELETE "/api/v1/packages/${owner}/nuget/${package_name}/${version}" "${body_file}" "${headers_file}")"
IFS=$'\t' read -r http_code status_text request_id <<< "${meta}"
rm -f "${body_file}" "${headers_file}"
if [[ "${http_code}" =~ ^2 ]]; then
log "Deleted package ${package_name} version ${version} -> ${status_text}"
deleted_count=$((deleted_count + 1))
else
if [[ -n "${API_REQUEST_ID}" ]]; then
log "ERROR: DELETE package ${name} version ${version} -> ${API_STATUS_TEXT} request_id=${API_REQUEST_ID}"
if [[ -n "${request_id}" ]]; then
log "ERROR: DELETE package ${package_name} version ${version} -> ${status_text} request_id=${request_id}"
else
log "ERROR: DELETE package ${name} version ${version} -> ${API_STATUS_TEXT}"
log "ERROR: DELETE package ${package_name} version ${version} -> ${status_text}"
fi
error_count=$((error_count + 1))
fi
done < "${CANDIDATES_FILE}"
done < "${candidate_file}"
log "Summary: packages=${PACKAGE_COUNT} versions=${TOTAL_VERSION_COUNT} kept=${KEPT_COUNT} candidates=${CANDIDATE_COUNT} deleted=${deleted_count} errors=${error_count}"
log "Summary: packages=${package_count} versions=${total_version_count} kept=${kept_count} candidates=${candidate_count} deleted=${deleted_count} errors=${error_count}"
}
main() {
local token keep_versions
local token keep_count repository owner repo package_names_csv
local candidate_file summary package_count total_version_count kept_count candidate_count
log "Gitea Server Url: ${GITEA_SERVER_URL:-}"
log "Gitea Repository: ${GITEA_REPOSITORY:-}"
@@ -247,27 +296,32 @@ main() {
fi
export RESOLVED_GITEA_TOKEN="$token"
init_repo_context
keep_versions="$(resolve_keep_versions)"
log "keep_count=${keep_versions}"
resolve_package_names
log "package_names=${INPUT_PACKAGE_NAMES}"
repository="${GITEA_REPOSITORY:-}"
IFS=$'\t' read -r owner repo <<< "$(parse_repo_context "${repository}")"
keep_count="$(resolve_keep_count)"
mapfile -t package_names < <(resolve_package_names)
package_names_csv="$(IFS=,; echo "${package_names[*]}")"
log "keep_count=${keep_count}"
log "package_names=${package_names_csv}"
log "Token source resolved successfully"
CANDIDATES_FILE="$(mktemp)"
export CANDIDATES_FILE
PACKAGE_COUNT=0
TOTAL_VERSION_COUNT=0
KEPT_COUNT=0
CANDIDATE_COUNT=0
trap 'rm -f "${CANDIDATES_FILE}"' EXIT
candidate_file="$(mktemp)"
trap "rm -f -- '${candidate_file}'" EXIT
collect_package_candidates
if (( PACKAGE_COUNT == 0 )); then
summary="$(collect_package_candidates "${owner}" "${keep_count}" "${candidate_file}" "${package_names[@]}")"
IFS=$'\t' read -r package_count total_version_count kept_count candidate_count <<< "${summary}"
if (( package_count == 0 )); then
log "No matching packages found for requested package_names"
fi
process_candidates
process_candidates "${owner}" "${candidate_file}" "${package_count}" "${total_version_count}" "${kept_count}" "${candidate_count}"
log "Stage 4 complete"
}
main "$@"
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
main "$@"
fi