From 528f8c75b33fcf043c6a347a873a80fe8fd8d1da Mon Sep 17 00:00:00 2001 From: Jeffery Date: Fri, 15 May 2026 02:50:38 +0000 Subject: [PATCH] feat: support package name filtering and refactor cleanup flow --- .gitea/workflows/ci.yaml | 17 +++ Dockerfile | 2 +- README.md | 2 + entrypoint.sh | 266 +++++++++++++++++++++++---------------- tests/entrypoint.sh | 223 ++++++++++++++++++++++++++++++++ 5 files changed, 403 insertions(+), 107 deletions(-) create mode 100644 .gitea/workflows/ci.yaml create mode 100644 tests/entrypoint.sh diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml new file mode 100644 index 0000000..73c711a --- /dev/null +++ b/.gitea/workflows/ci.yaml @@ -0,0 +1,17 @@ +name: CI + +on: + push: + branches: + - feat/解決問題 + pull_request: + +jobs: + test: + runs-on: ubuntu + steps: + - uses: actions/checkout@v4 + - name: Install jq + run: sudo apt-get update && sudo apt-get install -y jq + - name: Run shell tests + run: bash tests/entrypoint.sh diff --git a/Dockerfile b/Dockerfile index f6383ed..ca1906e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM alpine:latest -RUN apk add --no-cache --no-check-certificate bash curl jq ca-certificates +RUN apk add --no-cache bash curl jq ca-certificates COPY entrypoint.sh /entrypoint.sh diff --git a/README.md b/README.md index aa40027..985106e 100644 --- a/README.md +++ b/README.md @@ -59,3 +59,5 @@ jobs: - Action 定義: [`action.yaml`](action.yaml) - 執行腳本: [`entrypoint.sh`](entrypoint.sh) +- Shell 測試: [`tests/entrypoint.sh`](tests/entrypoint.sh) +- Gitea workflow: [`.gitea/workflows/ci.yaml`](.gitea/workflows/ci.yaml) diff --git a/entrypoint.sh b/entrypoint.sh index a364f3b..c58c047 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -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:-}" 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_codestatus_textrequest_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_counttotal_version_countkept_countcandidate_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 diff --git a/tests/entrypoint.sh b/tests/entrypoint.sh new file mode 100644 index 0000000..7eb6a4a --- /dev/null +++ b/tests/entrypoint.sh @@ -0,0 +1,223 @@ +#!/bin/bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "${TMP_DIR}"' EXIT + +assert_eq() { + local expected="$1" + local actual="$2" + + if [[ "${expected}" != "${actual}" ]]; then + printf 'Expected:\n%s\nActual:\n%s\n' "${expected}" "${actual}" >&2 + exit 1 + fi +} + +expect_fail() { + if ( "$@" ) >/dev/null 2>&1; then + printf 'Expected failure: %s\n' "$*" >&2 + exit 1 + fi +} + +write_mock_curl_pagination() { + cat > "${TMP_DIR}/curl" <<'EOF' +#!/bin/sh +make_versions_json() { + prefix="$1" + start="$2" + count="$3" + i=0 + printf '[' + while [ "$i" -lt "$count" ]; do + idx=$((start + i)) + [ "$i" -gt 0 ] && printf ',' + printf '{"name":"%s","version":"%s.%s.0","created_at":"2023-01-%02dT00:00:00Z"}' \ + "$prefix" "$idx" "$idx" "$((idx + 1))" + i=$((i + 1)) + done + printf ']' +} + +method=GET +out_file= +headers_file= +url= +while [ "$#" -gt 0 ]; do + case "$1" in + -X) method=$2; shift 2 ;; + -D) headers_file=$2; shift 2 ;; + -o) out_file=$2; shift 2 ;; + -w) shift 2 ;; + -H|-sS) shift 1 ;; + *) url=$1; shift ;; + esac +done + +body='[]' +status='HTTP/1.1 200 OK' +case "$url" in + *'/api/v1/packages/test-owner/nuget/pkg-a?page=1&limit=100') + body="$(make_versions_json pkg-a 0 100)" + ;; + *'/api/v1/packages/test-owner/nuget/pkg-a?page=2&limit=100') + body="$(make_versions_json pkg-a 100 1)" + ;; +esac + +printf '%s' "$body" > "$out_file" +{ + printf '%s\n' "$status" + printf 'X-Request-Id: req-page\n' +} > "$headers_file" + +printf '%s' 200 +EOF + chmod +x "${TMP_DIR}/curl" +} + +write_mock_curl_collect() { + cat > "${TMP_DIR}/curl" <<'EOF' +#!/bin/sh +method=GET +out_file= +headers_file= +url= +while [ "$#" -gt 0 ]; do + case "$1" in + -X) method=$2; shift 2 ;; + -D) headers_file=$2; shift 2 ;; + -o) out_file=$2; shift 2 ;; + -w) shift 2 ;; + -H|-sS) shift 1 ;; + *) url=$1; shift ;; + esac +done + +body='[]' +status='HTTP/1.1 200 OK' +code='200' +case "$url" in + *'/api/v1/packages/test-owner/nuget/pkg-a?page=1&limit=100') + body='[{"name":"pkg-a","version":"1.0.0","created_at":"2023-01-01T00:00:00Z"},{"name":"pkg-a","version":"1.1.0","created_at":"2023-01-02T00:00:00Z"},{"name":"pkg-a","version":"1.2.0","created_at":"2023-01-03T00:00:00Z"}]' + ;; + *'/api/v1/packages/test-owner/nuget/pkg-b?page=1&limit=100') + code='404' + status='HTTP/1.1 404 Not Found' + body='' + ;; +esac + +printf '%s' "$body" > "$out_file" +{ + printf '%s\n' "$status" + printf 'X-Request-Id: req-collect\n' +} > "$headers_file" + +printf '%s' "$code" +EOF + chmod +x "${TMP_DIR}/curl" +} + +write_mock_curl_delete() { + cat > "${TMP_DIR}/curl" <<'EOF' +#!/bin/sh +method=GET +out_file= +headers_file= +url= +while [ "$#" -gt 0 ]; do + case "$1" in + -X) method=$2; shift 2 ;; + -D) headers_file=$2; shift 2 ;; + -o) out_file=$2; shift 2 ;; + -w) shift 2 ;; + -H|-sS) shift 1 ;; + *) url=$1; shift ;; + esac +done + +code='200' +status='HTTP/1.1 200 OK' +body='[]' +case "$url" in + *'/api/v1/packages/test-owner/nuget/pkg-a?page=1&limit=100') + body='[{"name":"pkg-a","version":"1.0.0","created_at":"2023-01-01T00:00:00Z"},{"name":"pkg-a","version":"0.9.0","created_at":"2022-12-31T00:00:00Z"}]' + ;; + *'/api/v1/packages/test-owner/nuget/pkg-a/1.0.0') + if [ "$method" = "DELETE" ]; then + code='204' + status='HTTP/1.1 204 No Content' + body='' + fi + ;; + *'/api/v1/packages/test-owner/nuget/pkg-a/0.9.0') + if [ "$method" = "DELETE" ]; then + code='404' + status='HTTP/1.1 404 Not Found' + body='' + fi + ;; +esac + +printf '%s' "$body" > "$out_file" +{ + printf '%s\n' "$status" + printf 'X-Request-Id: req-delete\n' +} > "$headers_file" + +if [ "$method" = "DELETE" ] && [ -n "${MOCK_DELETE_LOG:-}" ]; then + printf '%s %s\n' "$method" "$url" >> "$MOCK_DELETE_LOG" +fi + +printf '%s' "$code" +EOF + chmod +x "${TMP_DIR}/curl" +} + +source "${ROOT_DIR}/entrypoint.sh" + +assert_eq "abc" "$(RUNNER_TOKEN=abc resolve_token)" +expect_fail env -u RUNNER_TOKEN bash -lc "source '${ROOT_DIR}/entrypoint.sh'; resolve_token" + +assert_eq "5" "$(INPUT_KEEP_COUNT=5 resolve_keep_count)" +expect_fail env INPUT_KEEP_COUNT=abc bash -lc "source '${ROOT_DIR}/entrypoint.sh'; resolve_keep_count" + +output="$(INPUT_PACKAGE_NAMES=$' pkg-a, pkg-b\npkg-a , pkg-c ' resolve_package_names)" +assert_eq $'pkg-a\npkg-b\npkg-c' "${output}" +expect_fail env INPUT_PACKAGE_NAMES=' , ' bash -lc "source '${ROOT_DIR}/entrypoint.sh'; resolve_package_names" + +IFS=$'\t' read -r owner repo <<< "$(parse_repo_context "owner/repo")" +assert_eq "owner" "${owner}" +assert_eq "repo" "${repo}" +expect_fail bash -lc "source '${ROOT_DIR}/entrypoint.sh'; parse_repo_context 'repo'" + +write_mock_curl_pagination +output="$(PATH="${TMP_DIR}:$PATH" GITEA_SERVER_URL="https://gitea.example.com" RESOLVED_GITEA_TOKEN="token" \ + fetch_package_versions "test-owner" "pkg-a")" +assert_eq "101" "$(jq 'length' <<<"${output}")" +assert_eq "0.0.0" "$(jq -r '.[0].version' <<<"${output}")" +assert_eq "100.100.0" "$(jq -r '.[100].version' <<<"${output}")" + +write_mock_curl_collect +summary="$(PATH="${TMP_DIR}:$PATH" GITEA_SERVER_URL="https://gitea.example.com" RESOLVED_GITEA_TOKEN="token" \ + collect_package_candidates "test-owner" "1" "${TMP_DIR}/candidates.tsv" "pkg-a" "pkg-b")" +IFS=$'\t' read -r package_count total_version_count kept_count candidate_count <<< "${summary}" +assert_eq "1" "${package_count}" +assert_eq "3" "${total_version_count}" +assert_eq "1" "${kept_count}" +assert_eq "2" "${candidate_count}" +assert_eq "2" "$(wc -l < "${TMP_DIR}/candidates.tsv" | tr -d ' ')" + +write_mock_curl_delete +candidate_file="${TMP_DIR}/delete.tsv" +cat > "${candidate_file}" <<'EOF' +pkg-a 1.0.0 2023-01-01T00:00:00Z +pkg-a 0.9.0 2022-12-31T00:00:00Z +EOF +output="$(MOCK_DELETE_LOG="${TMP_DIR}/delete.log" PATH="${TMP_DIR}:$PATH" GITEA_SERVER_URL="https://gitea.example.com" RESOLVED_GITEA_TOKEN="token" \ + process_candidates "test-owner" "${candidate_file}" "1" "2" "1" "2" 2>&1)" +assert_eq "1" "$(grep -c '^DELETE ' "${TMP_DIR}/delete.log")" +assert_eq "Summary: packages=1 versions=2 kept=1 candidates=2 deleted=1 errors=1" "$(printf '%s' "${output}" | tail -n 1)"