diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json new file mode 100644 index 0000000..e8744f3 --- /dev/null +++ b/.gitea/ai-review/exclusions.json @@ -0,0 +1,39 @@ +{ + "excluded_findings": [ + { + "location": "entrypoint.sh:10-15", + "title": "trim unit tests", + "reason": "This repository intentionally excludes test fixtures and CI workflows." + }, + { + "location": "entrypoint.sh:30-59", + "title": "resolve_package_names unit tests", + "reason": "This repository intentionally excludes test fixtures and CI workflows." + }, + { + "location": "entrypoint.sh:61-76", + "title": "parse_repo_context unit tests", + "reason": "This repository intentionally excludes test fixtures and CI workflows." + }, + { + "location": "entrypoint.sh:78-120", + "title": "api_request unit tests", + "reason": "This repository intentionally excludes test fixtures and CI workflows." + }, + { + "location": "entrypoint.sh:122-181", + "title": "fetch_package_versions unit tests", + "reason": "This repository intentionally excludes test fixtures and CI workflows." + }, + { + "location": "entrypoint.sh:183-241", + "title": "collect_package_candidates unit tests", + "reason": "This repository intentionally excludes test fixtures and CI workflows." + }, + { + "location": "entrypoint.sh:243-286", + "title": "process_candidates unit tests", + "reason": "This repository intentionally excludes test fixtures and CI workflows." + } + ] +} diff --git a/README.md b/README.md index f60617e..52f2891 100644 --- a/README.md +++ b/README.md @@ -65,3 +65,15 @@ jobs: - 不需要新增 `.gitea/workflows/ci.yaml` - 不需要新增 `tests/*` - 除非另外明確要求,請不要再補這兩類檔案 + +## Review Exclusions + +以下審查意見屬於刻意排除,不再以測試或 CI 的形式處理: + +- `entrypoint.sh:10-15` `trim` +- `entrypoint.sh:30-59` `resolve_package_names` +- `entrypoint.sh:61-76` `parse_repo_context` +- `entrypoint.sh:78-120` `api_request` +- `entrypoint.sh:122-181` `fetch_package_versions` +- `entrypoint.sh:183-241` `collect_package_candidates` +- `entrypoint.sh:243-286` `process_candidates` diff --git a/entrypoint.sh b/entrypoint.sh index c58c047..b0cb78b 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -11,13 +11,20 @@ fail() { } trim() { + # Remove leading and trailing ASCII whitespace from a string. local value="$1" value="${value#"${value%%[![:space:]]*}"}" value="${value%"${value##*[![:space:]]}"}" printf '%s' "$value" } +url_encode() { + # URL-encode a single path or query component using jq's RFC 3986 encoding. + jq -rn --arg value "$1" '$value|@uri' +} + resolve_token() { + # Resolve the already-merged token input passed in RUNNER_TOKEN. log "Trying token from RUNNER_TOKEN" if [[ -n "${RUNNER_TOKEN:-}" ]]; then @@ -30,6 +37,7 @@ resolve_token() { } resolve_keep_count() { + # Parse KEEP_COUNT and ensure it is a non-negative integer. local raw_value="${INPUT_KEEP_COUNT:-2}" if [[ -z "${raw_value}" ]]; then @@ -135,18 +143,26 @@ api_request() { 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. + # Params: + # $1 owner + # $2 package_name + # stdout: + # JSON array of version objects. local owner="$1" local package_name="$2" local page=1 local limit=100 local aggregate_file page_file headers_file meta http_code status_text request_id page_length path + local encoded_owner encoded_package_name + + encoded_owner="$(url_encode "${owner}")" + encoded_package_name="$(url_encode "${package_name}")" aggregate_file="$(mktemp)" printf '[]' > "${aggregate_file}" while :; do - path="/api/v1/packages/${owner}/nuget/${package_name}?page=${page}&limit=${limit}" + path="/api/v1/packages/${encoded_owner}/nuget/${encoded_package_name}?page=${page}&limit=${limit}" page_file="$(mktemp)" headers_file="$(mktemp)" meta="$(api_request GET "${path}" "${page_file}" "${headers_file}")" @@ -184,7 +200,13 @@ fetch_package_versions() { collect_package_candidates() { # Build the delete candidate file for the requested package names. - # stdout: package_counttotal_version_countkept_countcandidate_count + # Params: + # $1 owner + # $2 keep_count + # $3 candidate_file + # $4... package names + # stdout: + # package_counttotal_version_countkept_countcandidate_count local owner="$1" local keep_count="$2" local candidate_file="$3" @@ -215,7 +237,7 @@ collect_package_candidates() { 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' <<<"${versions_json}") + done < <(jq -r 'sort_by(.created_at, .version)[] | [.version, .created_at] | @tsv' <<<"${versions_json}") if (( total_versions <= keep_count )); then log " keep all ${total_versions} versions" @@ -226,7 +248,7 @@ collect_package_candidates() { kept_count=$((kept_count + keep_count)) candidates_json="$( jq -c --argjson keep "${keep_count}" \ - 'sort_by(.created_at) | .[0:(length - $keep)]' <<<"${versions_json}" + 'sort_by(.created_at, .version) | .[0:(length - $keep)]' <<<"${versions_json}" )" while IFS=$'\t' read -r package version created_at; do @@ -242,6 +264,13 @@ collect_package_candidates() { process_candidates() { # Delete each queued version and summarize the result. + # Params: + # $1 owner + # $2 candidate_file + # $3 package_count + # $4 total_version_count + # $5 kept_count + # $6 candidate_count local owner="$1" local candidate_file="$2" local package_count="$3" @@ -250,7 +279,7 @@ process_candidates() { local candidate_count="$6" local deleted_count=0 local error_count=0 - local package_name version created_at + local package_name version _created_at local body_file headers_file meta http_code status_text request_id if [[ ! -s "${candidate_file}" ]]; then @@ -259,12 +288,12 @@ process_candidates() { return 0 fi - while IFS=$'\t' read -r package_name version created_at; do + while IFS=$'\t' read -r package_name version _created_at; do [[ -z "${package_name}" ]] && continue body_file="$(mktemp)" headers_file="$(mktemp)" - meta="$(api_request DELETE "/api/v1/packages/${owner}/nuget/${package_name}/${version}" "${body_file}" "${headers_file}")" + meta="$(api_request DELETE "/api/v1/packages/$(url_encode "${owner}")/nuget/$(url_encode "${package_name}")/$(url_encode "${version}")" "${body_file}" "${headers_file}")" IFS=$'\t' read -r http_code status_text request_id <<< "${meta}" rm -f "${body_file}" "${headers_file}" @@ -285,7 +314,10 @@ process_candidates() { } main() { - local token keep_count repository owner repo package_names_csv + # Entry point for the Docker container. Resolves inputs, builds candidates, + # and applies deletes for the selected NuGet packages. + local token keep_count repository owner _repo package_names_csv + local -a package_names local candidate_file summary package_count total_version_count kept_count candidate_count log "Gitea Server Url: ${GITEA_SERVER_URL:-}" @@ -298,7 +330,7 @@ main() { export RESOLVED_GITEA_TOKEN="$token" repository="${GITEA_REPOSITORY:-}" - IFS=$'\t' read -r owner repo <<< "$(parse_repo_context "${repository}")" + IFS=$'\t' read -r owner _repo <<< "$(parse_repo_context "${repository}")" keep_count="$(resolve_keep_count)" mapfile -t package_names < <(resolve_package_names)