Repo tools: add parallel repository generation support (#9150)

**Repository tools: enable parallel repo generation and refactor repo.sh**

Add support for parallel repository generation using GitHub Actions, allowing
multiple workers to build different releases concurrently without database
locking conflicts.

### Highlights
- Add `-R/--single-release`, `update-main`, and `merge` for CI-level parallelism
- Use isolated per-release databases; remove local `-j/--parallel-jobs`
- Sign all Release files; optimize signing order
- Add `extract-repo.sh` for extracting and reorganizing packages from repositories
- Replace file-based logging with syslog (`logger`), removing sudo requirement
- Improve error handling, signing logic, and robustness
- Refactor repo.sh for better readability and documentation
- Apply CodeRabbit-recommended fixes

Signed-off-by: Igor Pecovnik <igor@armbian.com>
This commit is contained in:
Igor
2026-01-02 16:48:06 +01:00
committed by GitHub
parent f01745cfa9
commit 4bba1d3c5e
2 changed files with 1447 additions and 0 deletions

355
tools/repository/extract-repo.sh Executable file
View File

@@ -0,0 +1,355 @@
#!/usr/bin/env bash
# Extract packages from an existing Debian repository and organize them
# into the input structure expected by repo.sh
#
# Expected input structure:
# /root/*.deb -> main component (common across releases)
# /extra/{release}-utils/*.deb -> release-specific utils
# /extra/{release}-desktop/*.deb -> release-specific desktop
set -e
# Default values
REPO_URL=""
OUTPUT_DIR=""
RELEASES=()
VERBOSE=false
DRY_RUN=false
# Logging
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"
}
log_verbose() {
if [[ "$VERBOSE" == true ]]; then
log "$*"
fi
}
# Display help
show_help() {
cat << EOF
Extract packages from an existing Debian repository and organize them
for use as input to repo.sh
Usage: $0 -u <repo_url> -o <output_dir> [options]
Required:
-u, --url <path> Repository path (local directory)
-o, --output <dir> Output directory for extracted packages
Optional:
-r, --releases <list> Comma-separated list of releases to extract
(default: auto-detect from repository)
-v, --verbose Verbose output
--dry-run Show what would be done without actually doing it
-h, --help Show this help
Output Structure:
{output_dir}/
*.deb -> Packages for 'main' component
extra/
{release}-utils/ -> Release-specific utils packages
*.deb
{release}-desktop/ -> Release-specific desktop packages
*.deb
Examples:
# Extract from local repository
$0 -u /path/to/repo/public -o /tmp/extracted
# Extract specific releases only
$0 -u /path/to/repo/public -o /tmp/extracted -r jammy,noble,bookworm
# Dry-run to see what would be extracted
$0 -u /path/to/repo/public -o /tmp/extracted --dry-run
EOF
exit 0
}
# Parse command line arguments
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
-u|--url)
REPO_URL="$2"
shift 2
;;
-o|--output)
OUTPUT_DIR="$2"
shift 2
;;
-r|--releases)
IFS=',' read -r -a RELEASES <<< "$2"
shift 2
;;
-v|--verbose)
VERBOSE=true
shift
;;
--dry-run)
DRY_RUN=true
shift
;;
-h|--help)
show_help
;;
*)
echo "Unknown option: $1"
echo "Use -h or --help for usage information"
exit 1
;;
esac
done
# Validate required arguments
if [[ -z "$REPO_URL" ]]; then
echo "Error: Repository URL is required"
echo "Use -h or --help for usage information"
exit 1
fi
if [[ -z "$OUTPUT_DIR" ]]; then
echo "Error: Output directory is required"
echo "Use -h or --help for usage information"
exit 1
fi
if [[ ! -d "$REPO_URL" ]]; then
echo "Error: Repository path does not exist: $REPO_URL"
exit 1
fi
}
# Detect releases from repository
detect_releases() {
local repo_base="$1"
log "Detecting releases from repository..."
if [[ -d "$repo_base/dists" ]]; then
# Capture find/basename output and check for errors
local releases_output
local releases_exit_code
releases_output=$(find "$repo_base/dists" -maxdepth 1 -type d -not -name "dists" -exec basename {} \; 2>&1 | sort)
releases_exit_code=$?
if [[ $releases_exit_code -ne 0 ]]; then
log "Error: Failed to detect releases (find exit code: $releases_exit_code)" >&2
log "Output: $releases_output" >&2
DETECTED_RELEASES=()
return 1
fi
# Check if output is non-empty before feeding to mapfile
if [[ -n "$releases_output" ]]; then
mapfile -t DETECTED_RELEASES <<< "$releases_output"
else
DETECTED_RELEASES=()
fi
else
DETECTED_RELEASES=()
fi
if [[ ${#DETECTED_RELEASES[@]} -eq 0 ]]; then
log "Warning: Could not auto-detect releases"
DETECTED_RELEASES=()
else
log "Detected releases: ${DETECTED_RELEASES[*]}"
fi
}
# Get package list from Packages file
get_packages_from_component() {
local repo_base="$1"
local release="$2"
local component="$3"
# Find Packages file - try different architectures
local component_dir="$repo_base/dists/$release/$component"
if [[ ! -d "$component_dir" ]]; then
return
fi
# Find any Packages file in the component directory
local packages_file=$(find "$component_dir" -type f -name "Packages" | head -1)
if [[ -n "$packages_file" && -f "$packages_file" ]]; then
# Extract package filenames from Packages file
grep -E '^Filename:' "$packages_file" | sed 's/Filename: //' || true
fi
}
# Extract packages from repository
extract_packages() {
local repo_base="$1"
local output_base="$2"
log "Starting package extraction..."
log "Repository: $repo_base"
log "Output: $output_base"
# Create output directories
if [[ "$DRY_RUN" == false ]]; then
mkdir -p "$output_base/extra"
fi
local total_packages=0
local copied_packages=0
local skipped_packages=0
local error_packages=0
# Determine which releases to process
local releases_to_process=()
if [[ ${#RELEASES[@]} -gt 0 ]]; then
releases_to_process=("${RELEASES[@]}")
else
if [[ ${#DETECTED_RELEASES[@]} -eq 0 ]]; then
detect_releases "$repo_base"
fi
releases_to_process=("${DETECTED_RELEASES[@]}")
fi
if [[ ${#releases_to_process[@]} -eq 0 ]]; then
log "Error: No releases found to process"
exit 1
fi
log "Processing releases: ${releases_to_process[*]}"
# Process each release
for release in "${releases_to_process[@]}"; do
log "Processing release: $release"
# Define components to check
# 'main' component has packages that go to root
# '{release}-utils' and '{release}-desktop' have release-specific packages
local components=("main" "${release}-utils" "${release}-desktop")
for component in "${components[@]}"; do
log_verbose "Processing component: $release/$component"
# Get package list
mapfile -t packages < <(get_packages_from_component "$repo_base" "$release" "$component")
if [[ ${#packages[@]} -eq 0 ]]; then
log_verbose "No packages found for $release/$component"
continue
fi
log "Found ${#packages[@]} packages in $release/$component"
# Process each package
for package_path in "${packages[@]}"; do
((total_packages++)) || true
local package_name=$(basename "$package_path")
local source_path="$repo_base/$package_path"
# Determine target directory based on component
local target_dir=""
if [[ "$component" == "main" ]]; then
# Main component packages go to root
target_dir="$output_base"
else
# Release-specific components go to extra/
target_dir="$output_base/extra/$component"
if [[ "$DRY_RUN" == false ]]; then
mkdir -p "$target_dir"
fi
fi
local target_path="$target_dir/$package_name"
# Copy package
if [[ "$DRY_RUN" == true ]]; then
log "[DRY-RUN] Would copy: $package_name -> $target_dir"
((copied_packages++)) || true
else
if [[ -f "$source_path" ]]; then
# Check if file already exists and is identical
if [[ -f "$target_path" ]]; then
# Compare files
if cmp -s "$source_path" "$target_path"; then
log_verbose "Skipping (identical): $package_name"
((skipped_packages++)) || true
else
log_verbose "Copying (updated): $package_name"
# Try hard link first, fall back to copy
cp -l "$source_path" "$target_path" 2>/dev/null || cp "$source_path" "$target_path"
((copied_packages++)) || true
fi
else
log_verbose "Copying: $package_name"
# Try hard link first, fall back to copy
cp -l "$source_path" "$target_path" 2>/dev/null || cp "$source_path" "$target_path"
((copied_packages++)) || true
fi
else
log "Warning: Source file not found: $source_path"
((error_packages++)) || true
fi
fi
done
done
done
# Print summary
log "=========================================="
log "Extraction complete!"
log "Total packages found: $total_packages"
log "Packages copied: $copied_packages"
log "Packages skipped: $skipped_packages"
if [[ $error_packages -gt 0 ]]; then
log "Packages with errors: $error_packages"
fi
log "Output directory: $output_base"
log "=========================================="
# Show output structure
if [[ "$DRY_RUN" == false ]] && [[ -d "$output_base" ]]; then
log ""
log "Output structure:"
find "$output_base" -maxdepth 2 -type d | sed 's|'"$output_base"'||' | sort | while read -r dir; do
if [[ -n "$dir" ]]; then
local count=$(find "$output_base$dir" -maxdepth 1 -name "*.deb" 2>/dev/null | wc -l)
if [[ $count -gt 0 ]]; then
log " $dir: $count packages"
fi
fi
done
fi
}
# Main execution
main() {
parse_args "$@"
# Normalize repository URL
local repo_base="$REPO_URL"
# Remove trailing slash
repo_base="${repo_base%/}"
# Auto-detect releases if not specified
if [[ ${#RELEASES[@]} -eq 0 ]]; then
detect_releases "$repo_base"
RELEASES=("${DETECTED_RELEASES[@]}")
fi
log "Repository extraction configuration:"
log " Source: $repo_base"
log " Output: $OUTPUT_DIR"
log " Releases: ${RELEASES[*]:-auto-detect}"
log " Dry-run: $DRY_RUN"
log ""
# Perform extraction
extract_packages "$repo_base" "$OUTPUT_DIR"
}
# Run main function
main "$@"

1092
tools/repository/repo.sh Executable file

File diff suppressed because it is too large Load Diff