name: Update Nix Packages with nix-update on: schedule: - cron: "0 2,14 * * *" # Every 12 hours at 2 AM and 2 PM workflow_dispatch: inputs: package: description: "Specific package to update (optional)" required: false type: string concurrency: group: nix-update-${{ github.ref }} cancel-in-progress: true env: GIT_AUTHOR_NAME: "nix-update bot" GIT_AUTHOR_EMAIL: "bot@m3ta.dev" GIT_COMMITTER_NAME: "nix-update bot" GIT_COMMITTER_EMAIL: "bot@m3ta.dev" REPO_DIR: "/tmp/nixpkgs" # Nix configuration NIX_PATH: "nixpkgs=channel:nixos-unstable" NIX_CONFIG: "experimental-features = nix-command flakes" # Non-interactive mode DEBIAN_FRONTEND: "noninteractive" GIT_TERMINAL_PROMPT: "0" jobs: nix-update: runs-on: nixos timeout-minutes: 180 steps: - name: Setup Environment and Authenticate run: | if [ -d "$REPO_DIR" ]; then rm -rf "$REPO_DIR"; fi git config --global credential.helper store echo "https://m3tam3re:${{ secrets.NIX_UPDATE_TOKEN }}@code.m3ta.dev" > ~/.git-credentials chmod 600 ~/.git-credentials git config --global user.name "$GIT_AUTHOR_NAME" git config --global user.email "$GIT_AUTHOR_EMAIL" git config --global init.defaultBranch master - name: Checkout Repository run: | git clone --no-single-branch \ "https://m3tam3re@code.m3ta.dev/m3tam3re/nixpkgs.git" \ "$REPO_DIR" - name: Check Prerequisites id: check run: | cd "$REPO_DIR" if [ ! -d "pkgs" ]; then echo "❌ Error: 'pkgs' directory not found." exit 1 fi if [ -f "flake.nix" ]; then echo "has_flake=true" >> $GITHUB_OUTPUT else echo "has_flake=false" >> $GITHUB_OUTPUT fi - name: Update Packages id: update run: | cd "$REPO_DIR" set -e git checkout master UPDATES_FOUND=false UPDATED_PACKAGES="" check_commit() { [ "$1" != "$(git rev-parse HEAD)" ] && echo "true" || echo "false" } has_update_script() { local pkg=$1 # Check if package has passthru.updateScript attribute nix eval .#${pkg}.passthru.updateScript --json >/dev/null 2>&1 } run_update() { local pkg=$1 local before_hash=$(git rev-parse HEAD) echo "::group::Updating $pkg" local args=("--flake" "--commit" "--use-github-releases") # Handle subpackages (opencode has node_modules) if [ "$pkg" = "opencode" ]; then args+=("--subpackage" "node_modules") fi args+=("$pkg") if nix-update "${args[@]}" 2>&1 | tee /tmp/update-${pkg}.log; then if [ "$(check_commit "$before_hash")" = "true" ]; then echo "✅ Updated $pkg" echo "::endgroup::" return 0 fi fi echo "::endgroup::" if ! grep -q "already up to date\|No new version found" /tmp/update-${pkg}.log; then echo "⚠️ Update failed for $pkg" fi return 1 } if [ -n "${{ inputs.package }}" ]; then pkg="${{ inputs.package }}" if [ -d "pkgs/$pkg" ]; then if run_update "$pkg"; then UPDATES_FOUND=true UPDATED_PACKAGES="$pkg" fi else echo "❌ Package 'pkgs/$pkg' not found" fi else # Dynamically discover packages with updateScript attribute echo "🔍 Discovering packages with passthru.updateScript..." # Get all packages and filter those with updateScript ALL_PACKAGES=$(find pkgs -mindepth 1 -maxdepth 1 -type d -exec basename {} \; 2>/dev/null | sort) UPDATABLE_PACKAGES="" if [ -z "$ALL_PACKAGES" ]; then echo "No packages found in pkgs/" exit 0 fi for pkg in $ALL_PACKAGES; do if has_update_script "$pkg"; then echo " ✓ $pkg (has updateScript)" UPDATABLE_PACKAGES="$UPDATABLE_PACKAGES $pkg" else echo " ⊘ $pkg (no updateScript - skipping)" fi done if [ -z "$UPDATABLE_PACKAGES" ]; then echo "ℹ️ No packages with updateScript found." exit 0 fi echo "" echo "📦 Found $(echo $UPDATABLE_PACKAGES | wc -w) updatable packages" echo "" # Parallel updates with 4 concurrent jobs MAX_JOBS=4 JOB_COUNT=0 SUCCESS_LIST=() for pkg in $UPDATABLE_PACKAGES; do (run_update "$pkg" && echo "$pkg" >> /tmp/success.txt || true) & ((JOB_COUNT++)) # Wait if we hit max concurrent jobs if [ $JOB_COUNT -ge $MAX_JOBS ]; then wait JOB_COUNT=0 fi done # Wait for remaining jobs wait # Parse results if [ -f /tmp/success.txt ]; then SUCCESS_LIST=$(cat /tmp/success.txt | tr '\n' ' ') UPDATED_PACKAGES=$(echo "$SUCCESS_LIST" | sed 's/ /, /g' | sed 's/, $//') UPDATES_FOUND=true fi rm -f /tmp/success.txt fi COMMIT_COUNT=$(git rev-list --count origin/master..HEAD) if [ "$COMMIT_COUNT" -gt 0 ]; then echo "✅ $COMMIT_COUNT updates committed locally." echo "has_updates=true" >> $GITHUB_OUTPUT echo "updated_packages=${UPDATED_PACKAGES}" >> $GITHUB_OUTPUT else echo "ℹ️ No updates found." echo "has_updates=false" >> $GITHUB_OUTPUT fi - name: Verify Builds if: steps.update.outputs.has_updates == 'true' run: | cd "$REPO_DIR" echo "::group::Running flake check" if ! nix flake check; then echo "❌ Flake check failed" exit 1 fi echo "✅ Flake check passed" echo "::endgroup::" IFS=', ' read -ra PKGS <<< "${{ steps.update.outputs.updated_packages }}" FAILED_PACKAGES=() SUCCESSFUL_PACKAGES=() for pkg in "${PKGS[@]}"; do echo "::group::Building $pkg" if nix build .#$pkg 2>&1 | tee /tmp/build-${pkg}.log; then echo "✅ Build successful for $pkg" SUCCESSFUL_PACKAGES+=("$pkg") else echo "❌ Build failed for $pkg" FAILED_PACKAGES+=("$pkg") fi echo "::endgroup::" done if [ ${#FAILED_PACKAGES[@]} -gt 0 ]; then echo "" echo "❌ Failed packages: ${FAILED_PACKAGES[*]}" echo "✅ Successful packages: ${SUCCESSFUL_PACKAGES[*]}" echo "" # Upload logs as artifacts for debugging echo "## Build Failure Logs" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY for pkg in "${FAILED_PACKAGES[@]}"; do echo "### $pkg" >> $GITHUB_STEP_SUMMARY echo '```bash' >> $GITHUB_STEP_SUMMARY cat /tmp/build-${pkg}.log >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY done exit 1 fi echo "" echo "✅ All packages built successfully: ${SUCCESSFUL_PACKAGES[*]}" - name: Push Changes if: steps.update.outputs.has_updates == 'true' run: | cd "$REPO_DIR" PACKAGES="${{ steps.update.outputs.updated_packages }}" echo "::group::Git Operations" echo "Current commit: $(git rev-parse HEAD)" echo "Pending commits: $(git rev-list --count origin/master..HEAD)" echo "" echo "Pulling latest changes (rebase)..." if git pull --rebase origin master; then echo "✅ Rebase successful" else echo "⚠️ Rebase failed, attempting force push..." git reset --hard origin/master git push --force-with-lease origin master echo "✓ Force push completed" exit 0 fi echo "" echo "Pushing changes to master..." git push origin master echo "" echo "✅ Successfully pushed updates for: $PACKAGES" echo "::endgroup::" - name: Upload Build Logs if: failure() uses: actions/upload-artifact@v4 with: name: build-logs-${{ github.run_number }} path: | /tmp/update-*.log /tmp/build-*.log retention-days: 7 - name: Cleanup if: always() run: | # Remove git credentials securely rm -f ~/.git-credentials git config --global --unset credential.helper 2>/dev/null || true # Remove temporary directory rm -rf "$REPO_DIR" # Remove all log files rm -f /tmp/update-*.log /tmp/build-*.log /tmp/update-log.txt /tmp/success-packages.txt # Clear sensitive environment variables unset GIT_AUTHOR_EMAIL GIT_COMMITTER_EMAIL - name: Summary if: always() run: | if [ "${{ steps.update.outputs.has_updates }}" = "true" ]; then echo "# ✅ Update Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "## Updated Packages" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "\`${{ steps.update.outputs.updated_packages }}\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "## Status" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "- ✅ All packages validated with \`nix flake check\`" >> $GITHUB_STEP_SUMMARY echo "- ✅ All packages built successfully" >> $GITHUB_STEP_SUMMARY echo "- ✅ Changes pushed to master" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "## Workflow Performance" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "- Started: ${{ github.event.head_commit.timestamp }}" >> $GITHUB_STEP_SUMMARY echo "- Completed: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY echo "- Workflow Run: [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY else echo "# ℹ️ No Updates Required" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "No package updates found this run. All packages are up to date." >> $GITHUB_STEP_SUMMARY fi