soul-browser/.github/workflows/cleanup-r2-artifacts.yml
KaKi87 08c56be482 Add scheduled CI job to purge R2 APK fallbacks after 5 days.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-10 09:42:43 +02:00

128 lines
4.3 KiB
YAML

name: Cleanup R2 artifacts
on:
schedule:
# Daily; objects older than 5 days are removed.
- cron: '17 6 * * *'
workflow_dispatch:
jobs:
cleanup:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Delete soul-browser/ objects older than 5 days
env:
# Same credentials as the R2 fallback upload in build-apk.yml.
AWS_ACCESS_KEY_ID: ${{ vars.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
R2_ENDPOINT: https://673b6d22cf5cd200862a9c97dbe22ec6.r2.cloudflarestorage.com
R2_BUCKET: github-actions-artifacts
R2_PREFIX: soul-browser/
MAX_AGE_DAYS: '5'
run: |
set -euo pipefail
if [[ -z "${AWS_ACCESS_KEY_ID:-}" || -z "${AWS_SECRET_ACCESS_KEY:-}" ]]; then
echo "R2_ACCESS_KEY_ID (variable) / R2_SECRET_ACCESS_KEY (secret) are not configured." >&2
exit 1
fi
python3 <<'PY'
import json
import os
import subprocess
import sys
from datetime import datetime, timedelta, timezone
endpoint = os.environ["R2_ENDPOINT"]
bucket = os.environ["R2_BUCKET"]
prefix = os.environ["R2_PREFIX"]
max_age_days = int(os.environ["MAX_AGE_DAYS"])
summary_path = os.environ.get("GITHUB_STEP_SUMMARY", "")
if not prefix.startswith("soul-browser/"):
print(f"Refusing unsafe prefix: {prefix!r}", file=sys.stderr)
sys.exit(1)
cutoff = datetime.now(timezone.utc) - timedelta(days=max_age_days)
def aws_json(args: list[str]) -> dict:
raw = subprocess.check_output(
["aws", *args, "--endpoint-url", endpoint],
text=True,
)
return json.loads(raw) if raw.strip() else {}
def list_objects():
token = None
while True:
args = [
"s3api",
"list-objects-v2",
"--bucket",
bucket,
"--prefix",
prefix,
]
if token:
args.extend(["--continuation-token", token])
page = aws_json(args)
yield from page.get("Contents", [])
if not page.get("IsTruncated"):
break
token = page["NextContinuationToken"]
stale = []
scanned = 0
for obj in list_objects():
scanned += 1
key = obj["Key"]
if not key.startswith(prefix):
continue
last_modified = datetime.fromisoformat(
obj["LastModified"].replace("Z", "+00:00")
)
if last_modified < cutoff:
stale.append({"Key": key})
deleted = 0
for i in range(0, len(stale), 1000):
batch = stale[i : i + 1000]
result = aws_json(
[
"s3api",
"delete-objects",
"--bucket",
bucket,
"--delete",
json.dumps({"Objects": batch, "Quiet": True}),
]
)
errors = result.get("Errors") or []
if errors:
print(json.dumps(errors, indent=2), file=sys.stderr)
sys.exit(1)
deleted += len(batch)
lines = [
"## R2 artifact cleanup",
"",
f"- Prefix: `{prefix}`",
f"- Retention: {max_age_days} days (cutoff {cutoff.isoformat()})",
f"- Scanned: {scanned}",
f"- Deleted: {deleted}",
]
if deleted:
lines.append("")
lines.append("### Deleted keys")
for item in stale:
lines.append(f"- `{item['Key']}`")
text = "\n".join(lines) + "\n"
print(text)
if summary_path:
with open(summary_path, "a", encoding="utf-8") as fh:
fh.write(text)
PY