Check Package Versions #37
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Check Package Versions | |
| on: | |
| schedule: | |
| - cron: '0 6 * * *' # Daily at 6 AM UTC | |
| workflow_dispatch: | |
| inputs: | |
| package: | |
| description: 'Specific package to check (leave empty for all)' | |
| required: false | |
| type: string | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: write | |
| id-token: write | |
| jobs: | |
| collect-updates: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| updates: ${{ steps.collect.outputs.updates }} | |
| has_updates: ${{ steps.collect.outputs.has_updates }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Install dependencies | |
| run: sudo apt-get update && sudo apt-get install -y jq | |
| - name: Collect all package updates | |
| id: collect | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| INPUT_PACKAGE: ${{ inputs.package }} | |
| run: | | |
| # Source shared package configuration | |
| source ./scripts/packages/config.sh | |
| # Use check-upstream-versions.sh script to get updates | |
| UPDATES_JSON="[]" | |
| # Get list of packages to check | |
| if [[ -n "$INPUT_PACKAGE" ]]; then | |
| PACKAGES_TO_CHECK="$INPUT_PACKAGE" | |
| else | |
| PACKAGES_TO_CHECK="${!PACKAGE_REPOS[@]}" | |
| fi | |
| echo "Checking packages for upstream updates..." | |
| echo "" | |
| for pkg in $PACKAGES_TO_CHECK; do | |
| echo "Checking $pkg..." | |
| # Get spec version (version only, strip release) | |
| SPEC_VER=$(./scripts/packages/get-spec-version.sh "$pkg" 2>/dev/null | sed 's/-[^-]*$//') | |
| if [[ -z "$SPEC_VER" ]]; then | |
| echo " Could not get spec version" | |
| continue | |
| fi | |
| # Get upstream version | |
| UPSTREAM_VER=$(./scripts/packages/get-upstream-version.sh "$pkg" 2>/dev/null) | |
| if [[ -z "$UPSTREAM_VER" ]]; then | |
| echo " Could not get upstream version" | |
| continue | |
| fi | |
| echo " Spec: $SPEC_VER, Upstream: $UPSTREAM_VER" | |
| # Compare versions | |
| if [[ "$SPEC_VER" != "$UPSTREAM_VER" ]]; then | |
| echo " -> Needs update!" | |
| UPDATES_JSON=$(echo "$UPDATES_JSON" | jq -c \ | |
| --arg pkg "$pkg" \ | |
| --arg repo "${PACKAGE_REPOS[$pkg]}" \ | |
| --arg old "$SPEC_VER" \ | |
| --arg new "$UPSTREAM_VER" \ | |
| --arg deps "${PACKAGE_DEPS[$pkg]:-}" \ | |
| '. += [{"package": $pkg, "repo": $repo, "old_version": $old, "new_version": $new, "dependencies": $deps}]') | |
| fi | |
| done | |
| echo "" | |
| echo "Updates found:" | |
| echo "$UPDATES_JSON" | jq . | |
| if [[ "$UPDATES_JSON" == "[]" ]]; then | |
| echo "has_updates=false" >> $GITHUB_OUTPUT | |
| echo "updates=[]" >> $GITHUB_OUTPUT | |
| else | |
| echo "has_updates=true" >> $GITHUB_OUTPUT | |
| echo "updates=$(echo "$UPDATES_JSON" | jq -c .)" >> $GITHUB_OUTPUT | |
| fi | |
| group-updates: | |
| needs: collect-updates | |
| if: needs.collect-updates.outputs.has_updates == 'true' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| groups: ${{ steps.group.outputs.groups }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Group updates by dependency chain | |
| id: group | |
| run: | | |
| UPDATES='${{ needs.collect-updates.outputs.updates }}' | |
| # Source shared package configuration | |
| source ./scripts/packages/config.sh | |
| # Use PACKAGE_DEPS from shared config | |
| # Get list of packages that need updates | |
| UPDATED_PKGS=$(echo "$UPDATES" | jq -r '.[].package') | |
| # Function to get all ancestors (dependencies) of a package | |
| get_ancestors() { | |
| local pkg=$1 | |
| local ancestors="" | |
| for dep in ${PACKAGE_DEPS[$pkg]:-}; do | |
| ancestors="$ancestors $dep" | |
| ancestors="$ancestors $(get_ancestors $dep)" | |
| done | |
| echo "$ancestors" | |
| } | |
| # Function to get all descendants (dependents) of a package | |
| get_descendants() { | |
| local pkg=$1 | |
| local descendants="" | |
| for p in "${!PACKAGE_DEPS[@]}"; do | |
| if [[ " ${PACKAGE_DEPS[$p]} " == *" $pkg "* ]]; then | |
| descendants="$descendants $p" | |
| descendants="$descendants $(get_descendants $p)" | |
| fi | |
| done | |
| echo "$descendants" | |
| } | |
| # Build groups - packages that share dependencies go together | |
| declare -A VISITED | |
| GROUPS_JSON="[]" | |
| GROUP_ID=0 | |
| for pkg in $UPDATED_PKGS; do | |
| if [[ -n "${VISITED[$pkg]}" ]]; then | |
| continue | |
| fi | |
| # Start a new group with this package | |
| GROUP_PKGS="$pkg" | |
| VISITED[$pkg]=1 | |
| # Add all updated packages that are ancestors or descendants | |
| ancestors=$(get_ancestors "$pkg") | |
| descendants=$(get_descendants "$pkg") | |
| related="$ancestors $descendants" | |
| for other in $UPDATED_PKGS; do | |
| if [[ -z "${VISITED[$other]}" ]]; then | |
| # Check if other is related to this package | |
| if [[ " $related " == *" $other "* ]]; then | |
| GROUP_PKGS="$GROUP_PKGS $other" | |
| VISITED[$other]=1 | |
| # Also add relatives of this newly added package | |
| other_ancestors=$(get_ancestors "$other") | |
| other_descendants=$(get_descendants "$other") | |
| for another in $UPDATED_PKGS; do | |
| if [[ -z "${VISITED[$another]}" ]]; then | |
| if [[ " $other_ancestors $other_descendants " == *" $another "* ]]; then | |
| GROUP_PKGS="$GROUP_PKGS $another" | |
| VISITED[$another]=1 | |
| fi | |
| fi | |
| done | |
| fi | |
| fi | |
| done | |
| # Deduplicate and sort | |
| GROUP_PKGS=$(echo "$GROUP_PKGS" | tr ' ' '\n' | sort -u | tr '\n' ' ' | xargs) | |
| if [[ -n "$GROUP_PKGS" ]]; then | |
| # Build group JSON with package details | |
| GROUP_UPDATES="[]" | |
| for gpkg in $GROUP_PKGS; do | |
| PKG_DATA=$(echo "$UPDATES" | jq -c --arg pkg "$gpkg" '.[] | select(.package == $pkg)') | |
| if [[ -n "$PKG_DATA" ]]; then | |
| GROUP_UPDATES=$(echo "$GROUP_UPDATES" | jq -c --argjson data "$PKG_DATA" '. += [$data]') | |
| fi | |
| done | |
| GROUP_ID=$((GROUP_ID + 1)) | |
| GROUPS_JSON=$(echo "$GROUPS_JSON" | jq -c \ | |
| --argjson id "$GROUP_ID" \ | |
| --argjson packages "$GROUP_UPDATES" \ | |
| '. += [{"id": $id, "packages": $packages}]') | |
| fi | |
| done | |
| echo "Grouped updates:" | |
| echo "$GROUPS_JSON" | jq . | |
| echo "groups=$(echo "$GROUPS_JSON" | jq -c .)" >> $GITHUB_OUTPUT | |
| create-prs: | |
| needs: group-updates | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| max-parallel: 1 | |
| matrix: | |
| group: ${{ fromJson(needs.group-updates.outputs.groups) }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Install dependencies | |
| run: sudo apt-get update && sudo apt-get install -y jq | |
| - name: Generate group info | |
| id: info | |
| run: | | |
| PACKAGES='${{ toJson(matrix.group.packages) }}' | |
| # Get package info for branch name | |
| PKG_COUNT=$(echo "$PACKAGES" | jq 'length') | |
| # Create summary for PR title and branch name | |
| if [[ "$PKG_COUNT" -eq 1 ]]; then | |
| PKG_NAME=$(echo "$PACKAGES" | jq -r '.[0].package') | |
| NEW_VERSION=$(echo "$PACKAGES" | jq -r '.[0].new_version') | |
| TITLE="pkg(${PKG_NAME}): update to ${NEW_VERSION}" | |
| BRANCH="pkg-update/${PKG_NAME}-v${NEW_VERSION}" | |
| else | |
| # For multiple packages, use first package name and combine versions | |
| FIRST_PKG=$(echo "$PACKAGES" | jq -r '.[0].package') | |
| VERSIONS=$(echo "$PACKAGES" | jq -r '.[].new_version' | sort | tr '\n' '-' | sed 's/-$//') | |
| TITLE="pkg: update $FIRST_PKG and $((PKG_COUNT - 1)) related packages" | |
| BRANCH="pkg-update/${FIRST_PKG}-group-v${VERSIONS}" | |
| fi | |
| echo "title=$TITLE" >> $GITHUB_OUTPUT | |
| echo "branch=$BRANCH" >> $GITHUB_OUTPUT | |
| echo "pkg_count=$PKG_COUNT" >> $GITHUB_OUTPUT | |
| - name: Check for existing branch | |
| id: existing-pr | |
| run: | | |
| BRANCH="${{ steps.info.outputs.branch }}" | |
| # Check if branch already exists (locally or remotely) | |
| if git ls-remote --heads origin "$BRANCH" | grep -q .; then | |
| echo "Branch '$BRANCH' already exists - skipping PR creation" | |
| echo "exists=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "Branch '$BRANCH' does not exist - will create PR" | |
| echo "exists=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Update spec files | |
| if: steps.existing-pr.outputs.exists != 'true' | |
| run: | | |
| PACKAGES='${{ toJson(matrix.group.packages) }}' | |
| DATE=$(date "+%a %b %d %Y") | |
| # Use process substitution to avoid subshell issues | |
| while read -r pkg_data; do | |
| PKG=$(echo "$pkg_data" | jq -r '.package') | |
| OLD_VERSION=$(echo "$pkg_data" | jq -r '.old_version') | |
| NEW_VERSION=$(echo "$pkg_data" | jq -r '.new_version') | |
| SPEC_FILE="packages/$PKG/$PKG.spec" | |
| echo "Updating $PKG: $OLD_VERSION -> $NEW_VERSION" | |
| # Update Version field | |
| sed -i "s/^Version:.*$/Version: $NEW_VERSION/" "$SPEC_FILE" | |
| # Update Release back to 1 | |
| sed -i "s/^Release:.*$/Release: 1%{?dist}/" "$SPEC_FILE" | |
| # Add changelog entry | |
| CHANGELOG_ENTRY="* $DATE Hypercube <hypercube@binarypie.dev> - $NEW_VERSION-1\n- Update to $NEW_VERSION" | |
| sed -i "/%changelog/a\\$CHANGELOG_ENTRY" "$SPEC_FILE" | |
| done < <(echo "$PACKAGES" | jq -c '.[]') | |
| - name: Generate PR body | |
| if: steps.existing-pr.outputs.exists != 'true' | |
| id: body | |
| run: | | |
| PACKAGES='${{ toJson(matrix.group.packages) }}' | |
| { | |
| echo "body<<EOF" | |
| echo "## Package Updates" | |
| echo "" | |
| # Use process substitution to avoid subshell issues with while loop | |
| while read -r pkg_data; do | |
| PKG=$(echo "$pkg_data" | jq -r '.package') | |
| REPO=$(echo "$pkg_data" | jq -r '.repo') | |
| OLD=$(echo "$pkg_data" | jq -r '.old_version') | |
| NEW=$(echo "$pkg_data" | jq -r '.new_version') | |
| echo "### $PKG" | |
| echo "- **Version:** \`$OLD\` -> \`$NEW\`" | |
| echo "- **Release:** https://github.com/$REPO/releases/tag/v$NEW" | |
| echo "" | |
| done < <(echo "$PACKAGES" | jq -c '.[]') | |
| echo "---" | |
| echo "*This PR was automatically created by the package version checker.*" | |
| echo "*Packages are grouped by dependency chain to ensure correct build order.*" | |
| echo "EOF" | |
| } >> $GITHUB_OUTPUT | |
| - name: Create Pull Request | |
| if: steps.existing-pr.outputs.exists != 'true' | |
| id: create-pr | |
| uses: peter-evans/create-pull-request@v8 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| commit-message: "${{ steps.info.outputs.title }}" | |
| title: "${{ steps.info.outputs.title }}" | |
| body: ${{ steps.body.outputs.body }} | |
| branch: "${{ steps.info.outputs.branch }}" | |
| delete-branch: true | |
| labels: | | |
| dependencies | |
| package-update | |
| author: "Claude <claude@anthropic.com>" | |
| committer: "Claude <claude@anthropic.com>" | |
| - name: Fetch upstream build files | |
| if: steps.existing-pr.outputs.exists != 'true' && steps.create-pr.outputs.pull-request-number | |
| id: upstream | |
| run: | | |
| PACKAGES='${{ toJson(matrix.group.packages) }}' | |
| mkdir -p .upstream-build-files | |
| # Define build file patterns for different project types | |
| declare -A BUILD_FILES=( | |
| # Rust projects | |
| [eza]="Cargo.toml" | |
| [starship]="Cargo.toml" | |
| [wifitui]="Cargo.toml" | |
| # Go projects | |
| [lazygit]="go.mod" | |
| # Python projects | |
| [uwsm]="pyproject.toml" | |
| # Default for Hyprland ecosystem is CMakeLists.txt | |
| ) | |
| UPSTREAM_CONTENT="" | |
| while read -r pkg_data; do | |
| PKG=$(echo "$pkg_data" | jq -r '.package') | |
| REPO=$(echo "$pkg_data" | jq -r '.repo') | |
| VERSION=$(echo "$pkg_data" | jq -r '.new_version') | |
| # Determine which build file to fetch | |
| BUILD_FILE="${BUILD_FILES[$PKG]:-CMakeLists.txt}" | |
| echo "Fetching $BUILD_FILE for $PKG v$VERSION from $REPO..." | |
| # Try to fetch the build file | |
| URL="https://raw.githubusercontent.com/${REPO}/v${VERSION}/${BUILD_FILE}" | |
| CONTENT=$(curl -sL "$URL" 2>/dev/null) | |
| if [[ -z "$CONTENT" || "$CONTENT" == "404"* ]]; then | |
| # Try without 'v' prefix | |
| URL="https://raw.githubusercontent.com/${REPO}/${VERSION}/${BUILD_FILE}" | |
| CONTENT=$(curl -sL "$URL" 2>/dev/null) | |
| fi | |
| if [[ -n "$CONTENT" && "$CONTENT" != "404"* ]]; then | |
| # Save to file for Claude to read | |
| echo "$CONTENT" > ".upstream-build-files/${PKG}-${BUILD_FILE//\//-}" | |
| UPSTREAM_CONTENT="${UPSTREAM_CONTENT} | |
| === ${PKG} (${BUILD_FILE}) === | |
| ${CONTENT} | |
| " | |
| echo " ✓ Fetched successfully" | |
| else | |
| echo " ✗ Could not fetch $BUILD_FILE" | |
| UPSTREAM_CONTENT="${UPSTREAM_CONTENT} | |
| === ${PKG} (${BUILD_FILE}) === | |
| [Could not fetch - manual verification required] | |
| " | |
| fi | |
| done < <(echo "$PACKAGES" | jq -c '.[]') | |
| # Save all content to a file for the prompt | |
| echo "$UPSTREAM_CONTENT" > .upstream-build-files/all-content.txt | |
| - name: Analyze and update specs with Claude | |
| if: steps.existing-pr.outputs.exists != 'true' && steps.create-pr.outputs.pull-request-number | |
| uses: anthropics/claude-code-action@v1 | |
| with: | |
| claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| prompt: | | |
| I need you to analyze and update RPM spec files for package version upgrades. | |
| The following packages were updated (version bumps already applied to spec files): | |
| ${{ toJson(matrix.group.packages) }} | |
| The upstream build configuration files have been pre-fetched and saved to `.upstream-build-files/`. | |
| Read the file `.upstream-build-files/all-content.txt` to see all the upstream build files. | |
| For each package, please: | |
| 1. Read the upstream build configuration from `.upstream-build-files/all-content.txt` | |
| 2. Compare the dependencies in the upstream build files against the current spec file's BuildRequires and Requires sections. | |
| 3. Check if any shared library soname versions have changed (e.g., libfoo.so.3 -> libfoo.so.4) by looking at: | |
| - CMake's set_target_properties for SOVERSION | |
| - The %files section of the spec for current soname expectations | |
| 4. Update the spec file if needed: | |
| - Add/remove/update BuildRequires for new build dependencies | |
| - Add/remove/update Requires for new runtime dependencies | |
| - Update soname versions in %files section if the library major version changed | |
| - Update any hardcoded paths or version references | |
| 5. If you make changes, update the changelog entry to mention what changed beyond just the version bump. | |
| The spec files are located at: packages/<package-name>/<package-name>.spec | |
| Focus on correctness - it's better to flag something for manual review than to make an incorrect change. | |
| If you're uncertain about a change, add a comment in the PR explaining what might need manual verification. | |
| claude_args: "--max-turns 10" |