Skip to content

R2 cleanup (purge beta + prune old stable) #4

R2 cleanup (purge beta + prune old stable)

R2 cleanup (purge beta + prune old stable) #4

name: R2 cleanup (purge beta + prune old stable)
# 一次性/按需运维:清理 Cloudflare R2 (bucket: openspeech) 里的历史归档。
# - 永远删:所有 beta 版本前缀(v*-beta*/)
# - 可选删:正式版只保留最近 keep_stable 个(按 semver 排序),更老的正式版前缀也删
# - 永远留:最新 keep_stable 个正式版 + 顶层 manifest(latest*.json / download*.json)
# 复用 release.yml 同一套 R2_* secrets。
#
# 用法:Actions → 本 workflow → Run workflow
# 1. 先 mode=dry-run(配合 keep_stable)看 artifact 报告里的「将删除」清单与体积;
# 2. 确认后 mode=delete 真删。
#
# keep_stable=0 表示不动正式版(只清 beta,向后兼容旧行为)。
# 安全:delete step 拒绝删除「最新正式版」前缀(latest 通道指向它);删后自校验,
# 仍有 beta 版本 key 残留则 exit 1。
on:
workflow_dispatch:
inputs:
mode:
description: 'dry-run = 只列清单不删;delete = 真正删除'
required: true
type: choice
default: dry-run
options:
- dry-run
- delete
keep_stable:
description: '保留最近多少个正式版(0 = 全部保留,仅清 beta)'
required: true
type: string
default: '0'
permissions:
contents: read
jobs:
cleanup:
runs-on: ubuntu-latest
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_ENDPOINT_URL: ${{ secrets.R2_ENDPOINT }}
AWS_DEFAULT_REGION: auto
R2_BUCKET: ${{ secrets.R2_BUCKET }}
MODE: ${{ inputs.mode }}
KEEP_STABLE: ${{ inputs.keep_stable }}
steps:
- name: List & classify objects
run: |
set -euo pipefail
KEEP=${KEEP_STABLE:-0}
[[ "$KEEP" =~ ^[0-9]+$ ]] || KEEP=0
echo "Mode: $MODE | Bucket: $R2_BUCKET | keep_stable: $KEEP"
aws s3 ls "s3://$R2_BUCKET" --recursive > all_objects.txt
total_all=$(wc -l < all_objects.txt)
# 按 key 首段路径聚合;不含 / 的顶层文件归到 (root)
awk '{
key=$4; size=$3;
n=index(key,"/");
grp=(n>0)?substr(key,1,n-1):"(root)";
objs[grp]++; bytes[grp]+=size;
} END { for (g in objs) printf "%s\t%d\t%d\n", g, objs[g], bytes[g] }' \
all_objects.txt | sort > groups.txt
# 正式版列表(非 root、非 beta),按 semver 升序
awk -F'\t' '$1 != "(root)" && $1 !~ /-beta/ {print $1}' groups.txt | sort -V > stable.txt
n_stable=$(wc -l < stable.txt)
newest_stable=""
[[ "$n_stable" -gt 0 ]] && newest_stable=$(tail -1 stable.txt)
echo "$newest_stable" > newest_stable.txt
# 计算待删的「最老正式版」集合
: > drop_stable.txt
if [[ "$KEEP" -gt 0 && "$n_stable" -gt "$KEEP" ]]; then
drop=$((n_stable - KEEP))
head -n "$drop" stable.txt > drop_stable.txt
fi
: > del_prefixes.txt
: > report.txt
del_objs=0; del_bytes=0; del_count=0
keep_objs=0; keep_bytes=0
while IFS=$'\t' read -r g o b; do
tag=""
if [[ "$g" == "(root)" ]]; then
tag="[ROOT-KEEP]"
elif [[ "$g" == *-beta* ]]; then
tag="[BETA -> DELETE]"
elif grep -qxF "$g" drop_stable.txt; then
tag="[STABLE-OLD -> DELETE]"
else
tag="[STABLE-KEEP]"
fi
printf '%-34s objs=%-4s bytes=%s %s\n' "$g" "$o" "$b" "$tag" >> report.txt
if [[ "$tag" == *DELETE* ]]; then
echo "$g/" >> del_prefixes.txt
del_objs=$((del_objs+o)); del_bytes=$((del_bytes+b)); del_count=$((del_count+1))
else
keep_objs=$((keep_objs+o)); keep_bytes=$((keep_bytes+b))
fi
done < groups.txt
{
echo "total_all_objects=$total_all"
echo "keep_stable_target=$KEEP"
echo "stable_count_before=$n_stable"
echo "newest_stable=$newest_stable"
echo "delete_prefixes=$del_count"
echo "delete_objects=$del_objs"
echo "delete_bytes=$del_bytes"
echo "keep_objects=$keep_objs"
echo "keep_bytes=$keep_bytes"
} > totals.txt
{
echo "## R2 cleanup ($MODE, keep_stable=$KEEP)"
echo '```'
cat totals.txt
echo '```'
echo '```'
cat report.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
cat totals.txt
- name: Upload report artifact
uses: actions/upload-artifact@v4
with:
name: r2-cleanup-report
path: |
report.txt
del_prefixes.txt
totals.txt
retention-days: 7
- name: Delete prefixes
if: ${{ inputs.mode == 'delete' }}
run: |
set -euo pipefail
if [[ ! -s del_prefixes.txt ]]; then
echo "Nothing to delete."
exit 0
fi
newest=$(cat newest_stable.txt 2>/dev/null || true)
while IFS= read -r p; do
[[ -z "$p" ]] && continue
# 必须是版本前缀(以 / 结尾、v 开头),否则中止
case "$p" in
v*/) ;;
*) echo "ABORT: unexpected prefix in delete list: $p"; exit 1 ;;
esac
# 绝不删最新正式版(latest 通道指向它)
if [[ -n "$newest" && "$p" == "$newest/" ]]; then
echo "ABORT: refusing to delete newest stable: $p"; exit 1
fi
echo ">>> deleting s3://$R2_BUCKET/$p"
aws s3 rm "s3://$R2_BUCKET/$p" --recursive --only-show-errors
done < del_prefixes.txt
echo "Delete done."
- name: Verify after delete
if: ${{ inputs.mode == 'delete' }}
run: |
set -euo pipefail
# 只数版本前缀里的 beta key;根 manifest latest-beta.json/download-beta.json 名字含 -beta,须排除
remaining_beta=$(aws s3 ls "s3://$R2_BUCKET" --recursive | awk '{print $4}' | grep -cE '^v[0-9][^/]*-beta[^/]*/' || true)
stable_after=$(aws s3 ls "s3://$R2_BUCKET" --recursive | awk '{print $4}' \
| grep -E '^v[0-9]' | grep -vE '-beta' | sed -E 's#/.*#/#' | sort -u | grep -c . || true)
summ=$(aws s3 ls "s3://$R2_BUCKET" --recursive --summarize | tail -2)
bytes=$(printf '%s\n' "$summ" | awk '/Total Size/{print $NF}')
{
echo "## R2 cleanup verify"
echo "remaining_beta_keys=$remaining_beta"
echo "stable_prefixes_after=$stable_after"
echo "bucket_total_bytes=${bytes:-0}"
} >> "$GITHUB_STEP_SUMMARY"
echo "remaining_beta_keys=$remaining_beta"
echo "stable_prefixes_after=$stable_after"
echo "bucket_total_bytes=${bytes:-0}"
if [[ "$remaining_beta" -ne 0 ]]; then
echo "ERROR: still $remaining_beta beta keys left"; exit 1
fi
echo "OK."