Files
TheRock/build_tools/packaging/linux/upload_package_repo.py
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

783 lines
28 KiB
Python

#!/usr/bin/env python3
# Copyright Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
"""
Packaging + repository upload tool.
Usage (multi-arch CI — new-style, preferred):
python ./build_tools/packaging/linux/upload_package_repo.py \
--pkg-type deb \
--run-id 16418185899
Bucket + prefix are resolved automatically via WorkflowOutputRoot using
the GITHUB_REPOSITORY and RELEASE_TYPE environment variables:
- CI builds → therock-ci-artifacts / <run_id>-linux/packages/deb
- dev builds → therock-dev-artifacts / <run_id>-linux/packages/deb
- nightly → therock-nightly-artifacts / <run_id>-linux/packages/deb
- prerelease → therock-prerelease-artifacts / <run_id>-linux/packages/deb
Usage (single-arch release — legacy, build_native_linux_packages.yml):
python ./build_tools/packaging/linux/upload_package_repo.py \
--pkg-type deb \
--s3-bucket therock-nightly-packages \
--amdgpu-family gfx94X-dcgpu \
--artifact-id 16418185899 \
--job nightly
Legacy upload locations:
dev/nightly → s3bucket/<pkg_type>/<YYYYMMDD>-<artifact_id>
prerelease → s3bucket/v3/packages/<pkg_type>
"""
import argparse
import boto3
import datetime
import os
import shutil
import subprocess
import sys
from pathlib import Path
_THIS_DIR = Path(__file__).resolve().parent
_BUILD_TOOLS_DIR = _THIS_DIR.parent.parent
if str(_THIS_DIR) not in sys.path:
sys.path.insert(0, str(_THIS_DIR))
if str(_BUILD_TOOLS_DIR) not in sys.path:
sys.path.insert(0, str(_BUILD_TOOLS_DIR))
from _therock_utils.storage_location import StorageLocation
from _therock_utils.workflow_outputs import WorkflowOutputRoot
def regenerate_rpm_metadata_from_s3(s3, bucket, prefix, uploaded_packages):
"""Regenerate RPM repository metadata using merge approach.
Downloads existing repodata from S3, generates metadata for new packages,
merges them using mergerepo_c, and uploads the result back to S3.
Args:
s3: boto3 S3 client
bucket: S3 bucket name
prefix: S3 prefix (e.g., 'rpm/20251222-12345')
uploaded_packages: List of actually uploaded .rpm file paths
"""
import tempfile
print(f"Updating RPM repository metadata (merge mode)...")
# Create temporary directory for metadata operations
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Efficient approach: Download existing repodata and merge with new packages
old_repo_dir = temp_path / "old_repo"
new_repo_dir = temp_path / "new_repo"
merged_repo_dir = temp_path / "merged_repo"
old_repo_dir.mkdir(parents=True, exist_ok=True)
new_repo_dir.mkdir(parents=True, exist_ok=True)
merged_repo_dir.mkdir(parents=True, exist_ok=True)
# Step 1: Download existing repodata from S3 (small files)
old_repodata_dir = old_repo_dir / "repodata"
old_repodata_dir.mkdir(parents=True, exist_ok=True)
print(
f"Downloading existing repository metadata from S3: s3://{bucket}/{prefix}/x86_64/repodata/"
)
repodata_files = []
try:
paginator = s3.get_paginator("list_objects_v2")
for page in paginator.paginate(
Bucket=bucket, Prefix=f"{prefix}/x86_64/repodata/"
):
if "Contents" not in page:
continue
for obj in page["Contents"]:
key = obj["Key"]
filename = Path(key).name
local_file = old_repodata_dir / filename
s3.download_file(bucket, key, str(local_file))
repodata_files.append(filename)
print(f" Downloaded: {filename}")
if repodata_files:
print(
f"✅ Found {len(repodata_files)} existing metadata files to merge"
)
else:
print("No existing metadata files found")
except Exception as e:
print(f"⚠️ No existing repodata found (new repo?): {e}")
# Step 2: Generate repodata for NEW packages only (actually uploaded ones)
rpm_packages = [p for p in uploaded_packages if p.endswith(".rpm")]
if rpm_packages:
print(
f"Generating metadata for {len(rpm_packages)} uploaded RPM packages..."
)
# Copy uploaded RPMs to temp dir
new_arch_dir = new_repo_dir / "x86_64"
new_arch_dir.mkdir(parents=True, exist_ok=True)
for rpm_file in rpm_packages:
shutil.copy2(rpm_file, new_arch_dir / Path(rpm_file).name)
# Generate repodata for new packages with clean paths (no baseurl)
run_command(
["createrepo_c", "--no-database", "--simple-md-filenames", "."],
cwd=str(new_arch_dir),
)
print("✅ Generated metadata for uploaded packages")
else:
print("No new RPM packages uploaded (all deduplicated)")
# Still need to ensure old metadata is preserved!
if repodata_files:
print("Preserving existing repodata...")
# Just re-upload the existing repodata we downloaded
for metadata_file in old_repodata_dir.iterdir():
if metadata_file.is_file():
s3_key = f"{prefix}/x86_64/repodata/{metadata_file.name}"
s3.upload_file(str(metadata_file), bucket, s3_key)
print(f" Uploaded: {metadata_file.name}")
print("✅ RPM repository metadata preserved")
return
# Step 3: Merge repositories using mergerepo_c (no need to download all RPMs!)
merged_arch_dir = merged_repo_dir / "x86_64"
merged_arch_dir.mkdir(parents=True, exist_ok=True)
if repodata_files: # If we have existing metadata
print("Merging old and new repository metadata...")
# mergerepo_c merges repodata without needing actual RPM files!
# Use --no-database, --simple-md-filenames, and --omit-baseurl to ensure clean paths
run_command(
[
"mergerepo_c",
"--no-database",
"--simple-md-filenames",
"--omit-baseurl",
"--repo",
str(old_repo_dir),
"--repo",
str(new_repo_dir / "x86_64"),
"--outputdir",
str(merged_arch_dir),
],
cwd=str(temp_path),
)
print("✅ Merged repository metadata")
else: # First upload, no existing metadata
print("First upload - using new repository metadata")
shutil.copytree(
new_repo_dir / "x86_64" / "repodata", merged_arch_dir / "repodata"
)
# Step 4: Upload merged repodata to S3
merged_repodata = merged_arch_dir / "repodata"
if merged_repodata.exists():
print("Uploading merged repository metadata to S3...")
uploaded_metadata = []
for metadata_file in merged_repodata.iterdir():
if metadata_file.is_file():
s3_key = f"{prefix}/x86_64/repodata/{metadata_file.name}"
s3.upload_file(str(metadata_file), bucket, s3_key)
uploaded_metadata.append(metadata_file.name)
print(f" Uploaded: {metadata_file.name}")
print(f"✅ RPM repository metadata updated: {len(uploaded_metadata)} files")
def generate_release_file_with_checksums(release_file, job_type, dists_dir):
"""Generate a Debian Release file with MD5Sum, SHA1, and SHA256 checksums.
Args:
release_file: Path to the Release file to create
job_type: Job type for metadata (nightly/dev/release)
dists_dir: Directory containing Packages files (main/binary-amd64/)
"""
import hashlib
# Files to hash (relative paths from dists/stable/)
files_to_hash = [
(dists_dir / "Packages", "main/binary-amd64/Packages"),
(dists_dir / "Packages.gz", "main/binary-amd64/Packages.gz"),
]
# Calculate all hashes
md5_entries = []
sha1_entries = []
sha256_entries = []
for file_path, rel_path in files_to_hash:
if not file_path.exists():
continue
# Get file size
file_size = file_path.stat().st_size
# Calculate hashes
md5_hash = hashlib.md5()
sha1_hash = hashlib.sha1()
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as f:
while True:
data = f.read(65536) # Read in 64KB chunks
if not data:
break
md5_hash.update(data)
sha1_hash.update(data)
sha256_hash.update(data)
# Store entries (space-aligned format)
md5_entries.append(f" {md5_hash.hexdigest()} {file_size:16d} {rel_path}")
sha1_entries.append(f" {sha1_hash.hexdigest()} {file_size:16d} {rel_path}")
sha256_entries.append(f" {sha256_hash.hexdigest()} {file_size:16d} {rel_path}")
# Write Release file
with open(release_file, "w", encoding="utf-8") as f:
# Header fields
f.write(
f"""Origin: AMD ROCm
Label: ROCm {job_type} Packages
Suite: stable
Codename: stable
Architectures: amd64
Components: main
Description: ROCm APT Repository
Date: {datetime.datetime.utcnow():%a, %d %b %Y %H:%M:%S UTC}
"""
)
# MD5Sum section
if md5_entries:
f.write("MD5Sum:\n")
f.write("\n".join(md5_entries))
f.write("\n")
# SHA1 section
if sha1_entries:
f.write("SHA1:\n")
f.write("\n".join(sha1_entries))
f.write("\n")
# SHA256 section
if sha256_entries:
f.write("SHA256:\n")
f.write("\n".join(sha256_entries))
f.write("\n")
print(f"✅ Release file generated with checksums: MD5, SHA1, SHA256")
def upload_deb_metadata_to_s3(s3, bucket, prefix, dists_dir, release_file):
"""Helper function to upload Debian metadata files to S3.
Args:
s3: boto3 S3 client
bucket: S3 bucket name
prefix: S3 prefix
dists_dir: Directory containing Packages files
release_file: Path to Release file
"""
packages_file = dists_dir / "Packages"
packages_gz = dists_dir / "Packages.gz"
uploaded_count = 0
if packages_file.exists():
s3_key = f"{prefix}/dists/stable/main/binary-amd64/Packages"
s3.upload_file(str(packages_file), bucket, s3_key)
print(f" Uploaded: Packages")
uploaded_count += 1
if packages_gz.exists():
s3_key = f"{prefix}/dists/stable/main/binary-amd64/Packages.gz"
s3.upload_file(str(packages_gz), bucket, s3_key)
print(f" Uploaded: Packages.gz")
uploaded_count += 1
if release_file.exists():
s3_key = f"{prefix}/dists/stable/Release"
s3.upload_file(str(release_file), bucket, s3_key)
print(f" Uploaded: Release")
uploaded_count += 1
print(f"✅ DEB repository metadata updated: {uploaded_count} files")
def regenerate_deb_metadata_from_s3(
s3, bucket, prefix, uploaded_packages, job_type="nightly"
):
"""Regenerate Debian repository metadata efficiently with proper checksums.
Uses dpkg-scanpackages for efficiency (no package downloads), but generates
proper Release file with MD5Sum, SHA1, and SHA256 checksums.
Args:
s3: boto3 S3 client
bucket: S3 bucket name
prefix: S3 prefix (e.g., 'deb/20251222-12345')
uploaded_packages: List of actually uploaded .deb file paths
job_type: Job type for Release file metadata (default: 'nightly')
"""
import tempfile
print(f"Updating DEB repository metadata (merge mode with checksums)...")
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Setup directories
dists_dir = temp_path / "dists" / "stable" / "main" / "binary-amd64"
dists_dir.mkdir(parents=True, exist_ok=True)
pool_dir = temp_path / "pool" / "main"
pool_dir.mkdir(parents=True, exist_ok=True)
# Step 1: Download existing Packages file from S3 (SMALL FILE - efficient!)
existing_packages = dists_dir / "Packages.old"
packages_s3_key = f"{prefix}/dists/stable/main/binary-amd64/Packages"
try:
print(
f"Downloading existing Packages file from S3: s3://{bucket}/{packages_s3_key}"
)
s3.download_file(bucket, packages_s3_key, str(existing_packages))
with open(existing_packages, "r", encoding="utf-8") as f:
content = f.read()
pkg_count = content.count("\nPackage: ")
print(f"✅ Downloaded existing Packages file ({pkg_count} packages)")
except Exception as e:
print(f"⚠️ No existing Packages file found (new repo?): {e}")
existing_packages = None
# Step 2: Generate Packages entries for NEW packages only
deb_packages = [p for p in uploaded_packages if p.endswith(".deb")]
if deb_packages:
print(
f"Generating Packages entries for {len(deb_packages)} uploaded DEB packages..."
)
# Copy uploaded DEBs to temp dir
for deb_file in deb_packages:
shutil.copy2(deb_file, pool_dir / Path(deb_file).name)
# Generate Packages entries for uploaded packages
new_packages = dists_dir / "Packages.new"
run_command(
["dpkg-scanpackages", "-m", "pool/main", "/dev/null"],
cwd=str(temp_path),
stdout=new_packages,
)
print("✅ Generated Packages entries for uploaded packages")
else:
print("No new DEB packages uploaded (all deduplicated)")
if existing_packages and existing_packages.exists():
print("Preserving existing metadata...")
shutil.copy2(existing_packages, dists_dir / "Packages")
run_command(
["gzip", "-9c", "Packages"],
cwd=str(dists_dir),
stdout=dists_dir / "Packages.gz",
)
# Generate Release file with checksums
release_dir = temp_path / "dists" / "stable"
release_dir.mkdir(parents=True, exist_ok=True)
release_file = release_dir / "Release"
generate_release_file_with_checksums(release_file, job_type, dists_dir)
# Upload preserved files
upload_deb_metadata_to_s3(s3, bucket, prefix, dists_dir, release_file)
return
# Step 3: Merge old and new Packages files
merged_packages = dists_dir / "Packages"
if existing_packages and existing_packages.exists():
print("Merging old and new Packages files...")
def parse_packages_file(filepath):
"""Parse Packages file into dict keyed by Filename"""
packages = {}
with open(filepath, "r", encoding="utf-8") as f:
current_entry = []
current_filename = None
for line in f:
if line.strip() == "":
if current_entry and current_filename:
packages[current_filename] = (
"\n".join(current_entry) + "\n"
)
current_entry = []
current_filename = None
else:
current_entry.append(line.rstrip())
if line.startswith("Filename:"):
current_filename = line.split(":", 1)[1].strip()
if current_entry and current_filename:
packages[current_filename] = "\n".join(current_entry) + "\n"
return packages
old_packages = parse_packages_file(existing_packages)
new_packages_dict = parse_packages_file(new_packages)
print(f" Old metadata: {len(old_packages)} packages")
print(f" New metadata: {len(new_packages_dict)} packages")
merged = old_packages.copy()
merged.update(new_packages_dict)
with open(merged_packages, "w", encoding="utf-8") as outfile:
for filename in sorted(merged.keys()):
outfile.write(merged[filename])
outfile.write("\n")
print(f"✅ Merged Packages files: {len(merged)} total packages")
else:
print("First upload - using new Packages file")
shutil.copy2(new_packages, merged_packages)
# Compress Packages file
run_command(
["gzip", "-9c", "Packages"],
cwd=str(dists_dir),
stdout=dists_dir / "Packages.gz",
)
# Step 4: Generate Release file with checksums
release_dir = temp_path / "dists" / "stable"
release_dir.mkdir(parents=True, exist_ok=True)
release_file = release_dir / "Release"
generate_release_file_with_checksums(release_file, job_type, dists_dir)
# Step 5: Upload merged files to S3
upload_deb_metadata_to_s3(s3, bucket, prefix, dists_dir, release_file)
def regenerate_repo_metadata_from_s3(
s3, bucket, prefix, pkg_type, uploaded_packages, job_type="nightly"
):
"""Regenerate repository metadata efficiently using merge approach.
This uses mergerepo_c (RPM) or merges Packages files (DEB) to efficiently
update metadata without re-downloading all packages from S3.
Args:
s3: boto3 S3 client
bucket: S3 bucket name
prefix: S3 prefix (e.g., 'rpm/20251222-12345')
pkg_type: Package type ('rpm' or 'deb')
uploaded_packages: List of actually uploaded package file paths (avoids duplicates from deduplication)
job_type: Job type for Release file metadata (default: 'nightly')
"""
if pkg_type == "rpm":
regenerate_rpm_metadata_from_s3(s3, bucket, prefix, uploaded_packages)
elif pkg_type == "deb":
regenerate_deb_metadata_from_s3(s3, bucket, prefix, uploaded_packages, job_type)
else:
raise ValueError(f"Unsupported package type: {pkg_type}")
def run_command(cmd: list[str], cwd=None, stdout=None):
"""Run a command safely without shell interpolation.
Args:
cmd: Command and arguments as a list of strings
cwd: Working directory for the command
stdout: Optional path to redirect stdout to a file
Raises:
subprocess.CalledProcessError: If the command exits with non-zero status
"""
print(f"Running: {' '.join(cmd)}")
if stdout is not None:
with open(stdout, "wb") as f:
subprocess.run(cmd, check=True, cwd=cwd, stdout=f)
else:
subprocess.run(cmd, check=True, cwd=cwd)
def find_package_dir():
base = Path.cwd() / "output" / "packages"
if not base.exists():
raise RuntimeError(f"Package directory not found: {base}")
return base
def yyyymmdd():
return datetime.datetime.utcnow().strftime("%Y%m%d")
def s3_object_exists(s3, bucket, key):
try:
s3.head_object(Bucket=bucket, Key=key)
return True
except s3.exceptions.ClientError as e:
if e.response["Error"]["Code"] == "404":
return False
raise
def create_deb_repo(package_dir, job_type):
print("Creating APT repository...")
package_path = Path(package_dir)
dists = package_path / "dists" / "stable" / "main" / "binary-amd64"
pool = package_path / "pool" / "main"
dists.mkdir(parents=True, exist_ok=True)
pool.mkdir(parents=True, exist_ok=True)
for f in package_path.iterdir():
if f.suffix == ".deb":
shutil.move(f, pool / f.name)
run_command(
["dpkg-scanpackages", "-m", "pool/main", "/dev/null"],
cwd=str(package_path),
stdout=dists / "Packages",
)
run_command(
["gzip", "-9c", "Packages"],
cwd=str(dists),
stdout=dists / "Packages.gz",
)
release = package_path / "dists" / "stable" / "Release"
with open(release, "w", encoding="utf-8") as f:
f.write(
f"""Origin: AMD ROCm
Label: ROCm {job_type} Packages
Suite: stable
Codename: stable
Architectures: amd64
Components: main
Date: {datetime.datetime.utcnow():%a, %d %b %Y %H:%M:%S UTC}
"""
)
def create_rpm_repo(package_dir):
"""Create RPM repository structure.
Note: Repository metadata (repodata) will be regenerated from S3 after upload
to ensure it reflects all packages, including deduplicated ones.
"""
print("Creating RPM repository...")
package_path = Path(package_dir)
arch_dir = package_path / "x86_64"
arch_dir.mkdir(parents=True, exist_ok=True)
for f in package_path.iterdir():
if f.suffix == ".rpm":
shutil.move(f, arch_dir / f.name)
# Generate initial repodata from local packages with clean paths (no baseurl)
# This will be regenerated from S3 state after upload
run_command(
["createrepo_c", "--no-database", "--simple-md-filenames", "."],
cwd=str(arch_dir),
)
def upload_to_s3(source_dir, bucket, prefix, dedupe=False):
s3 = boto3.client("s3")
print(f"Uploading to s3://{bucket}/{prefix}/")
print(f"Deduplication: {'ON' if dedupe else 'OFF'}")
skipped = 0
uploaded = 0
uploaded_packages = [] # Track actually uploaded package files
for root, _, files in os.walk(source_dir):
for fname in files:
# Always skip local index.html files, those are generated server-side.
if fname == "index.html":
continue
# Skip build manifest files - these are for local tracking only
if fname.lower().endswith(".txt"):
print(f"Skipping build manifest file (local only): {fname}")
continue
local = Path(root) / fname
rel = local.relative_to(source_dir)
key = f"{prefix}/{rel.as_posix()}"
# Skip metadata files - they'll be regenerated/merged properly later
# For DEB: skip Packages, Packages.gz, Release in dists/
# For RPM: skip repodata/* files
if "/repodata/" in key or key.endswith("/repodata"):
print(f"Skipping metadata file (will regenerate): {fname}")
continue
if "/dists/" in key and (
fname in ["Packages", "Packages.gz", "Release", "InRelease"]
):
print(f"Skipping metadata file (will regenerate): {fname}")
continue
if dedupe and (fname.endswith(".deb") or fname.endswith(".rpm")):
if s3_object_exists(s3, bucket, key):
print(f"Skipping existing package: {fname}")
skipped += 1
continue
extra = {"ContentType": "text/html"} if fname.endswith(".html") else None
print(f"Uploading: {key}")
s3.upload_file(str(local), bucket, key, ExtraArgs=extra)
uploaded += 1
# Track uploaded packages for metadata generation
if fname.endswith(".deb") or fname.endswith(".rpm"):
uploaded_packages.append(str(local))
print(f"Uploaded: {uploaded}, Skipped: {skipped}")
if uploaded_packages:
print(f"Uploaded packages: {[Path(p).name for p in uploaded_packages]}")
return s3, uploaded_packages # Return S3 client and list of uploaded packages
def _emit_github_output(key: str, value: str) -> None:
"""Write a key=value pair to $GITHUB_OUTPUT if running in GitHub Actions."""
github_output = os.environ.get("GITHUB_OUTPUT")
if github_output:
with open(github_output, "a", encoding="utf-8") as f:
f.write(f"{key}={value}\n")
def _package_install_url(bucket: str, prefix: str, pkg_type: str) -> str:
"""Compute the package manager install URL for a given repo location.
For RPM repos, dnf/yum baseurl must point to the x86_64/ subdirectory
(the directory containing repodata/). For DEB repos, apt points to the
repo root (it resolves dists/ itself).
"""
base = StorageLocation(bucket, prefix).https_url
if pkg_type == "rpm":
return f"{base}/x86_64"
return base
def _resolve_upload_target(
args: argparse.Namespace,
pkg_type: str,
) -> tuple[str, str, str, bool, str]:
"""Resolve S3 bucket, prefix, install URL, dedupe flag, and job type.
Returns:
Tuple of (bucket, prefix, install_url, dedupe, job_type)
"""
if args.run_id:
# New-style: derive bucket + prefix from WorkflowOutputRoot.
# This is the single source of truth for CI path layout.
root = WorkflowOutputRoot.from_workflow_run(
run_id=args.run_id, platform="linux"
)
loc = root.native_linux_packages(pkg_type)
job_type = os.environ.get("RELEASE_TYPE", "ci")
install_url = _package_install_url(loc.bucket, loc.relative_path, pkg_type)
return loc.bucket, loc.relative_path, install_url, True, job_type
if args.s3_prefix:
# Legacy: explicit prefix provided by get_s3_config.py
return (
args.s3_bucket,
args.s3_prefix,
_package_install_url(args.s3_bucket, args.s3_prefix, pkg_type),
True,
args.job,
)
if args.job in ("nightly", "dev"):
prefix = f"{pkg_type}/{yyyymmdd()}-{args.artifact_id}"
return (
args.s3_bucket,
prefix,
_package_install_url(args.s3_bucket, prefix, pkg_type),
True,
args.job,
)
if args.job == "prerelease":
prefix = f"v3/packages/{pkg_type}"
return (
args.s3_bucket,
prefix,
_package_install_url(args.s3_bucket, prefix, pkg_type),
True,
args.job,
)
raise ValueError(f"Unknown job type: {args.job!r}")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--pkg-type", required=True, choices=["deb", "rpm"])
# New-style args: use WorkflowOutputRoot for bucket/prefix resolution.
# When --run-id is provided, bucket and prefix are derived automatically
# from CI context (GITHUB_REPOSITORY, RELEASE_TYPE, fork detection).
parser.add_argument(
"--run-id",
help="GitHub Actions workflow run ID (enables WorkflowOutputRoot path resolution)",
)
# Legacy args: kept for backward compatibility with build_native_linux_packages.yml
parser.add_argument("--s3-bucket")
parser.add_argument("--amdgpu-family", required=False) # unused, kept for compat
parser.add_argument("--artifact-id")
parser.add_argument(
"--job",
default="dev",
choices=["dev", "nightly", "prerelease"],
help="Job type (legacy, used when --run-id is not provided)",
)
parser.add_argument(
"--s3-prefix",
required=False,
help="Override S3 prefix (legacy, used when --run-id is not provided)",
)
parser.add_argument(
"--package-dir",
required=True,
help="Path to the directory containing built packages.",
)
args = parser.parse_args()
package_dir = Path(args.package_dir).resolve()
bucket, prefix, install_url, dedupe, job_type = _resolve_upload_target(
args, args.pkg_type
)
if args.pkg_type == "deb":
create_deb_repo(package_dir, job_type)
else:
create_rpm_repo(package_dir)
# Upload packages and metadata to S3
s3_client, uploaded_packages = upload_to_s3(
package_dir, bucket, prefix, dedupe=dedupe
)
# Efficiently update repository metadata by merging with existing metadata
regenerate_repo_metadata_from_s3(
s3_client, bucket, prefix, args.pkg_type, uploaded_packages, job_type
)
print(f"Package repository URL: {install_url}")
_emit_github_output("package_repository_url", install_url)
if __name__ == "__main__":
main()