Files
android-assistant/ci/r2_upload.py
T

121 lines
3.8 KiB
Python

"""Cloudflare R2 artifact upload with credentials from GCP Secrets Manager."""
import mimetypes
from typing import Any
from pathlib import Path
import boto3 # type: ignore[import-untyped]
from google.cloud import secretmanager # type: ignore[import-untyped]
# GCP Secret Manager secret names for R2 credentials
_R2_ACCESS_KEY_SECRET = "r2-access-key-id"
_R2_SECRET_KEY_SECRET = "r2-secret-access-key"
_R2_ENDPOINT_SECRET = "r2-endpoint-url"
_R2_BUCKET_SECRET = "r2-bucket-name"
def _fetch_secret(client: Any, project_id: str, secret_id: str) -> str:
"""Fetch the latest version of a secret from GCP Secrets Manager.
Args:
client: Secret Manager client.
project_id: GCP project ID.
secret_id: Name of the secret to retrieve.
Returns:
The secret value as a string.
Raises:
google.api_core.exceptions.NotFound: If the secret does not exist.
"""
name = f"projects/{project_id}/secrets/{secret_id}/versions/latest"
response = client.access_secret_version(request={"name": name})
return response.payload.data.decode("utf-8")
def get_r2_credentials(gcp_project_id: str) -> dict[str, str]:
"""Retrieve all Cloudflare R2 credentials from GCP Secrets Manager.
Args:
gcp_project_id: GCP project ID containing the secrets.
Returns:
Dictionary with keys: access_key_id, secret_access_key,
endpoint_url, bucket_name.
"""
client: Any = secretmanager.SecretManagerServiceClient() # pyright: ignore
return {
"access_key_id": _fetch_secret(client, gcp_project_id, _R2_ACCESS_KEY_SECRET),
"secret_access_key": _fetch_secret(
client, gcp_project_id, _R2_SECRET_KEY_SECRET
),
"endpoint_url": _fetch_secret(client, gcp_project_id, _R2_ENDPOINT_SECRET),
"bucket_name": _fetch_secret(client, gcp_project_id, _R2_BUCKET_SECRET),
}
def upload_to_r2(
release_dir: Path,
gcp_project_id: str,
run_id: str,
) -> list[str]:
"""Upload release artifacts to Cloudflare R2.
Fetches R2 credentials from GCP Secrets Manager, then uploads all
files in the release directory to the R2 bucket under a builds/<run_id>/
prefix.
Args:
release_dir: Directory containing release files to upload.
gcp_project_id: GCP project ID for Secrets Manager lookups.
run_id: Unique identifier for this build run.
Returns:
List of uploaded R2 object keys.
Raises:
FileNotFoundError: If release_dir does not exist.
botocore.exceptions.ClientError: If R2 upload fails.
"""
if not release_dir.is_dir():
raise FileNotFoundError(f"Release directory not found: {release_dir}")
credentials = get_r2_credentials(gcp_project_id)
s3_client: Any = boto3.client( # pyright: ignore
"s3",
endpoint_url=credentials["endpoint_url"],
aws_access_key_id=credentials["access_key_id"],
aws_secret_access_key=credentials["secret_access_key"],
)
bucket = credentials["bucket_name"]
prefix = f"builds/{run_id}"
uploaded_keys: list[str] = []
for file_path in sorted(release_dir.iterdir()):
if not file_path.is_file():
continue
key = f"{prefix}/{file_path.name}"
content_type, _ = mimetypes.guess_type(str(file_path))
extra_args: dict[str, str] = {}
if content_type:
extra_args["ContentType"] = content_type
print(f" Uploading {file_path.name} → {key}")
s3_client.upload_file( # pyright: ignore[reportUnknownMemberType]
str(file_path),
bucket,
key,
ExtraArgs=extra_args,
)
uploaded_keys.append(key)
print(
f"Uploaded {len(uploaded_keys)} files to R2 bucket '{bucket}' under '{prefix}/'"
)
return uploaded_keys