Files
RawMark/.gitea/workflows/cleanup-ghcr.yml
T
2026-10-30 09:51:23 +08:00

125 lines
6.2 KiB
YAML

name: Authorized RawMark GHCR cleanup
on:
workflow_dispatch:
permissions:
contents: read
jobs:
cleanup:
runs-on: docker
timeout-minutes: 15
container:
image: docker:29-cli
volumes:
- /docker/gitea/mirrors/RawMark:/docker/gitea/mirrors/RawMark
steps:
- name: Install Python
run: apk add --no-cache python3
- name: Verify shared token and remove only the authorized RawMark package
env:
ALL_GITHUB_TOKEN: ${{ secrets.ALL_GITHUB_TOKEN }}
run: |
python3 - <<'PYTHON'
import fcntl
import json
import os
import urllib.error
import urllib.request
from datetime import datetime, timezone
TOKEN = os.environ['ALL_GITHUB_TOKEN']
if not TOKEN:
raise RuntimeError('Missing account-level ALL_GITHUB_TOKEN')
BASE = 'https://api.github.com'
def api(path, method='GET'):
request = urllib.request.Request(BASE + path, method=method, headers={
'Authorization': 'Bearer ' + TOKEN,
'Accept': 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
'User-Agent': 'RawMark-authorized-package-cleanup',
})
try:
with urllib.request.urlopen(request, timeout=45) as response:
raw = response.read()
return response.status, response.headers, json.loads(raw) if raw else None
except urllib.error.HTTPError as error:
raise RuntimeError(f'GitHub {method} {path} failed: HTTP {error.code}') from None
def pages(path):
rows = []
for page in range(1, 1001):
separator = '&' if '?' in path else '?'
status, _, batch = api(f'{path}{separator}per_page=100&page={page}')
if status != 200 or not isinstance(batch, list):
raise RuntimeError('Invalid paginated GitHub response')
rows.extend(batch)
if len(batch) < 100:
return rows
raise RuntimeError('Pagination exceeded the safe limit')
def targets():
return [package for package in pages('/user/packages?package_type=container')
if package['name'].lower() == 'rawmark']
def snapshot(package):
if package['owner']['login'].lower() != 'eezd' or package['package_type'] != 'container':
raise RuntimeError('Unexpected package owner or type')
repository = package.get('repository')
if repository and repository['full_name'].lower() != 'eezd/rawmark':
raise RuntimeError('Package is linked to an unexpected repository')
return {
'id': package['id'], 'name': package['name'],
'visibility': package['visibility'],
'versions': [{'id': version['id'], 'name': version['name'],
'tags': version.get('metadata', {}).get('container', {}).get('tags', [])}
for version in pages('/user/packages/container/rawmark/versions')],
}
with open('/docker/gitea/mirrors/RawMark/.mirror.lock', 'a') as lock:
fcntl.flock(lock, fcntl.LOCK_EX)
_, headers, user = api('/user')
if user['login'].lower() != 'eezd':
raise RuntimeError('ALL_GITHUB_TOKEN must belong to eezd')
scopes = {scope.strip() for scope in headers.get('X-OAuth-Scopes', '').split(',')}
permissions = {
'repository_scope': 'repo' in scopes,
'workflow_scope': 'workflow' in scopes,
'read_packages': bool(scopes & {'read:packages', 'write:packages'}),
'write_packages': 'write:packages' in scopes,
'delete_packages': 'delete:packages' in scopes,
}
print('Verified token identity and declared permissions: ' + json.dumps({'login': user['login'], **permissions}), flush=True)
if not permissions['read_packages'] or not permissions['delete_packages']:
raise RuntimeError('GHCR cleanup requires read:packages and delete:packages')
_, _, repository = api('/repos/eezd/RawMark')
if not repository.get('permissions', {}).get('push'):
raise RuntimeError('ALL_GITHUB_TOKEN lacks RawMark repository write access')
packages = targets()
if len(packages) > 1:
raise RuntimeError('Multiple matching packages require investigation')
before = snapshot(packages[0]) if packages else None
print('Authorized GHCR cleanup inventory: ' + json.dumps(before), flush=True)
deleted = False
if before:
latest = targets()
if len(latest) != 1 or snapshot(latest[0]) != before:
raise RuntimeError('Package inventory changed before deletion')
status, _, _ = api('/user/packages/container/rawmark', 'DELETE')
if status != 204:
raise RuntimeError('Package deletion did not return HTTP 204')
deleted = True
if targets():
raise RuntimeError('RawMark GHCR package still appears after cleanup')
releases = pages('/repos/eezd/RawMark/releases')
tags = pages('/repos/eezd/RawMark/tags')
if releases or tags:
raise RuntimeError('Unexpected RawMark releases or tags remain')
result = {
'checked_at': datetime.now(timezone.utc).isoformat(),
'owner': user['login'], 'package': 'rawmark', 'permissions': permissions,
'repository_write_verified': True, 'before': before, 'deleted': deleted,
'remaining_packages': 0, 'releases': 0, 'tags': 0,
}
print('RAWMARK_CLEANUP_RESULT=' + json.dumps(result, sort_keys=True), flush=True)
PYTHON