128 lines
4.3 KiB
YAML
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
|