Files
nixpkgs/.gitea/workflows/nix-update.yml
m3tm3re 839d7ae743 feat(ci): support custom update scripts for specific packages
Add detection logic to distinguish between:
- nix-update-script (standard packages like beads, td, mem0, etc.)
- Custom scripts (like n8n's update.sh that strips 'n8n@' prefix)

For custom scripts, run them directly with standard env vars:
- UPDATE_NIX_NAME, UPDATE_NIX_PNAME
- UPDATE_NIX_OLD_VERSION, UPDATE_NIX_ATTR_PATH

This ensures n8n only gets stable releases via its custom script.
2026-02-20 09:01:06 +01:00

542 lines
20 KiB
YAML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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