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: Update All Flake Inputs id: update-flake-inputs run: | cd "$REPO_DIR" echo "::group::Discovering version-pinned flake inputs" # Get GitHub inputs with version refs (e.g., v1.2.9) VERSIONED_INPUTS=$(nix flake metadata --json | jq -r ' .locks.nodes | to_entries[] | select(.value.original.type == "github") | select(.value.original.ref != null) | select(.value.original.ref | test("^v?[0-9]+\\.[0-9]+")) | "\(.key) \(.value.original.owner) \(.value.original.repo) \(.value.original.ref)" ') echo "Discovered version-pinned inputs:" echo "$VERSIONED_INPUTS" echo "::endgroup::" UPDATED_INPUTS="" FAILED_INPUTS="" # Update each version-pinned input while read -r INPUT_NAME OWNER REPO CURRENT_REF; do [ -z "$INPUT_NAME" ] && continue echo "::group::Checking $INPUT_NAME ($OWNER/$REPO)" # Get latest stable release (exclude prereleases) # The /releases/latest endpoint already returns the latest non-prerelease, non-draft release LATEST=$(curl -sf "https://api.github.com/repos/$OWNER/$REPO/releases/latest" | \ jq -r 'if .prerelease == false then .tag_name else empty end') if [ -z "$LATEST" ]; then echo "⚠️ No stable release found for $INPUT_NAME (repo may only have prereleases)" FAILED_INPUTS="$FAILED_INPUTS $INPUT_NAME(no-stable-release)" echo "::endgroup::" continue fi echo "Current: $CURRENT_REF | Latest: $LATEST" if [ "$LATEST" != "$CURRENT_REF" ]; then echo "Updating $INPUT_NAME from $CURRENT_REF to $LATEST" # Update flake.nix sed -i "s|github:$OWNER/$REPO/[^\"']*|github:$OWNER/$REPO/$LATEST|g" flake.nix # Update flake.lock for this input if nix flake update "$INPUT_NAME" 2>&1 | tee /tmp/input-update.log; then UPDATED_INPUTS="$UPDATED_INPUTS $INPUT_NAME($LATEST)" echo "✅ Updated $INPUT_NAME to $LATEST" else echo "❌ Failed to update $INPUT_NAME" FAILED_INPUTS="$FAILED_INPUTS $INPUT_NAME(update-failed)" git checkout flake.nix flake.lock 2>/dev/null || true fi else echo "✓ $INPUT_NAME is already up to date" fi echo "::endgroup::" done <<< "$VERSIONED_INPUTS" echo "::group::Updating non-version-pinned inputs" # Update all non-version-pinned inputs (branches, no-ref) nix flake update echo "::endgroup::" # Check if we have any changes if [ -n "$(git status --porcelain flake.nix flake.lock)" ]; then echo "::group::Committing flake input updates" nix fmt flake.nix git add flake.nix flake.lock COMMIT_MSG="chore: update flake inputs" [ -n "$UPDATED_INPUTS" ] && COMMIT_MSG="$COMMIT_MSG - $(echo $UPDATED_INPUTS | tr ' ' ', ')" git commit -m "$COMMIT_MSG" echo "flake_inputs_updated=true" >> $GITHUB_OUTPUT echo "updated_inputs=${UPDATED_INPUTS# }" >> $GITHUB_OUTPUT [ -n "$FAILED_INPUTS" ] && echo "failed_inputs=${FAILED_INPUTS# }" >> $GITHUB_OUTPUT echo "::endgroup::" else echo "flake_inputs_updated=false" >> $GITHUB_OUTPUT fi - 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 } # Check if updateScript is a custom script (path-based) vs nix-update-script is_custom_update_script() { local pkg=$1 local result # nix-update-script returns a list like [ "/nix/store/...-nix-update/bin/nix-update" ] # Custom scripts return a path like "/nix/store/.../update.sh" result=$(nix eval --impure --raw --expr " let flake = builtins.getFlake (toString ./.); pkg = flake.packages.\${builtins.currentSystem}.${pkg}; script = pkg.passthru.updateScript or []; in if builtins.isPath script then \"custom\" else if builtins.isList script && builtins.length script > 0 then let first = builtins.head script; in if builtins.isString first && builtins.match \".*/nix-update$\" first != null then \"nix-update-script\" else if builtins.isPath first then \"custom\" else \"other\" else if builtins.isAttrs script && script ? command then if builtins.isPath script.command then \"custom\" else if builtins.isList script.command && builtins.isPath (builtins.head script.command) then \"custom\" else \"other\" else \"other\" " 2>/dev/null || echo "other") [[ "$result" == "custom" ]] } # Run a custom update script directly (for packages like n8n) run_custom_update_script() { local pkg=$1 local before_hash=$(git rev-parse HEAD) echo " 🔧 Detected custom update script for $pkg" # Get package metadata for environment variables local name pname version name=$(nix eval --raw .#${pkg}.name 2>/dev/null || echo "$pkg") pname=$(nix eval --raw .#${pkg}.pname 2>/dev/null || echo "$pkg") version=$(nix eval --raw .#${pkg}.version 2>/dev/null || echo "unknown") # Run the custom script using nix develop if nix develop --impure --expr " with builtins; let flake = getFlake (toString ./.); pkgs = flake.inputs.nixpkgs.legacyPackages.\${currentSystem}; pkg' = flake.packages.\${currentSystem}.${pkg}; script = pkg'.passthru.updateScript; cmd = if isAttrs script then script.command else script; scriptPath = if isList cmd then head cmd else cmd; in pkgs.mkShell { inputsFrom = [pkg']; packages = with pkgs; [ curl jq git ]; } " --command bash -c " export UPDATE_NIX_NAME='${name}' export UPDATE_NIX_PNAME='${pname}' export UPDATE_NIX_OLD_VERSION='${version}' export UPDATE_NIX_ATTR_PATH='${pkg}' # Get the script path and execute it script_path=\$(nix eval --impure --raw --expr ' let flake = builtins.getFlake (toString ./.); pkg = flake.packages.\${builtins.currentSystem}.${pkg}; script = pkg.passthru.updateScript; cmd = if builtins.isAttrs script then script.command else script; in if builtins.isList cmd then toString (builtins.head cmd) else toString cmd ' 2>/dev/null) if [ -n \"\$script_path\" ]; then echo \"Running: \$script_path\" bash \"\$script_path\" fi " 2>&1 | tee /tmp/update-${pkg}.log; then if [ "$(check_commit "$before_hash")" = "true" ]; then echo "✅ Updated $pkg (via custom script)" return 0 fi fi # Clean up on failure git checkout -- . 2>/dev/null || true git clean -fd 2>/dev/null || true if ! grep -q "already up to date\|No new version found" /tmp/update-${pkg}.log; then echo "⚠️ Custom update script failed for $pkg" fi return 1 } run_update() { local pkg=$1 local before_hash=$(git rev-parse HEAD) echo "::group::Updating $pkg" # Check if this package has a custom update script if is_custom_update_script "$pkg"; then if run_custom_update_script "$pkg"; then echo "::endgroup::" return 0 else echo "::endgroup::" return 1 fi fi # Standard nix-update for packages with nix-update-script local args=("--flake" "--commit" "--use-github-releases") 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 # Clean up any uncommitted changes from failed update git checkout -- . 2>/dev/null || true git clean -fd 2>/dev/null || true 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 "" for pkg in $UPDATABLE_PACKAGES; do if run_update "$pkg"; then UPDATES_FOUND=true if [ -n "$UPDATED_PACKAGES" ]; then UPDATED_PACKAGES="$UPDATED_PACKAGES, $pkg" else UPDATED_PACKAGES="$pkg" fi fi done 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' || steps.update-flake-inputs.outputs.flake_inputs_updated == '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' || steps.update-flake-inputs.outputs.flake_inputs_updated == 'true' run: | cd "$REPO_DIR" PACKAGES="${{ steps.update.outputs.updated_packages }}" if [ "${{ steps.update-flake-inputs.outputs.flake_inputs_updated }}" = "true" ]; then UPDATED_INPUTS="${{ steps.update-flake-inputs.outputs.updated_inputs }}" if [ -n "$PACKAGES" ]; then PACKAGES="$PACKAGES, flake inputs ($UPDATED_INPUTS)" else PACKAGES="flake inputs ($UPDATED_INPUTS)" fi fi 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, resetting and retrying..." git rebase --abort 2>/dev/null || true git reset --hard origin/master echo "❌ Could not rebase, updates lost. Will retry next run." exit 0 fi echo "" echo "Pushing changes to master..." git push origin master echo "" echo "✅ Successfully pushed updates for: $PACKAGES" echo "::endgroup::" - 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/opencode-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: | HAS_UPDATES="false" if [ "${{ steps.update.outputs.has_updates }}" = "true" ]; then HAS_UPDATES="true" 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 fi if [ "${{ steps.update-flake-inputs.outputs.flake_inputs_updated }}" = "true" ]; then HAS_UPDATES="true" echo "" >> $GITHUB_STEP_SUMMARY echo "## Updated Flake Inputs" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY UPDATED_INPUTS="${{ steps.update-flake-inputs.outputs.updated_inputs }}" if [ -n "$UPDATED_INPUTS" ]; then echo "$UPDATED_INPUTS" | tr ' ' '\n' | while read -r input; do [ -n "$input" ] && echo "- **$input**" >> $GITHUB_STEP_SUMMARY done fi FAILED_INPUTS="${{ steps.update-flake-inputs.outputs.failed_inputs }}" if [ -n "$FAILED_INPUTS" ]; then echo "" >> $GITHUB_STEP_SUMMARY echo "### Failed Inputs" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "$FAILED_INPUTS" | tr ' ' '\n' | while read -r input; do [ -n "$input" ] && echo "- $input" >> $GITHUB_STEP_SUMMARY done fi fi if [ "$HAS_UPDATES" = "true" ]; then echo "" >> $GITHUB_STEP_SUMMARY echo "## Status" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "- ✅ All updates validated with \`nix flake check\`" >> $GITHUB_STEP_SUMMARY echo "- ✅ All builds successful" >> $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 updates found this run. All packages and flake inputs are up to date." >> $GITHUB_STEP_SUMMARY fi