name: Weekly Flake Update on: schedule: # Every Friday at 22:00 UTC - cron: '0 22 * * 5' workflow_dispatch: {} jobs: check-updates: runs-on: nix outputs: changed: ${{ steps.changes.outputs.changed }} hosts: ${{ steps.hosts.outputs.hosts || '[]' }} steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Configure Git run: | git config user.name "Flake Update Bot" git config user.email "bot@noreply.local" - name: Update flake inputs run: nix flake update - name: Check for changes id: changes run: | echo "DEBUG: GITHUB_OUTPUT='${GITHUB_OUTPUT:-}'" echo "DEBUG: Checking if GITHUB_OUTPUT var is set: $(if [ -z "${GITHUB_OUTPUT+x}" ]; then echo "NOT SET"; else echo "SET (value: '$GITHUB_OUTPUT')"; fi)" set_output() { echo "::set-output name=$1::$2" if [ -n "${GITHUB_OUTPUT:-}" ]; then echo "$1=$2" >> "$GITHUB_OUTPUT" fi } if git diff --quiet flake.lock; then set_output changed false else set_output changed true fi - name: Extract LXC hosts id: hosts if: steps.changes.outputs.changed == 'true' run: | set -euo pipefail set_output() { echo "::set-output name=$1::$2" if [ -n "${GITHUB_OUTPUT:-}" ]; then echo "$1=$2" >> "$GITHUB_OUTPUT" fi } HOSTS=$(nix eval --json .#colmena --apply 'x: builtins.filter (n: n != "meta" && builtins.elem "lxc" (x.${n}.deployment.tags or [])) (builtins.attrNames x)') || { echo "Failed to evaluate colmena hosts" exit 1 } # Validate the output is a non-empty JSON array if ! echo "$HOSTS" | jq -e '. | if type == "array" and length > 0 then true else false end' > /dev/null 2>&1; then echo "Error: No LXC hosts found or invalid JSON output: $HOSTS" set_output hosts "[]" exit 0 fi set_output hosts "$HOSTS" echo "Discovered hosts: $HOSTS" # Also write to file as backup (in case job outputs don't work) echo "$HOSTS" > hosts.json # Debug output echo "DEBUG: GITHUB_OUTPUT exists: ${GITHUB_OUTPUT:-NOT SET}" if [ -n "${GITHUB_OUTPUT:-}" ] && [ -f "$GITHUB_OUTPUT" ]; then echo "DEBUG: GITHUB_OUTPUT contents:" cat "$GITHUB_OUTPUT" fi - name: Debug job outputs if: steps.changes.outputs.changed == 'true' run: | echo "=== Step outputs ===" echo "steps.changes.outputs.changed = '${{ steps.changes.outputs.changed }}'" echo "steps.hosts.outputs.hosts = '${{ steps.hosts.outputs.hosts }}'" echo "Length of hosts output: ${#HOSTS_OUT}" env: HOSTS_OUT: ${{ steps.hosts.outputs.hosts }} - name: Upload flake.lock if: steps.changes.outputs.changed == 'true' uses: forgejo/upload-artifact@v4 with: name: flake-lock path: flake.lock retention-days: 1 - name: Upload hosts list if: steps.changes.outputs.changed == 'true' uses: forgejo/upload-artifact@v4 with: name: hosts-list path: hosts.json retention-days: 1 build: needs: check-updates if: needs.check-updates.outputs.changed == 'true' && needs.check-updates.outputs.hosts != '[]' && needs.check-updates.outputs.hosts != '' runs-on: nix strategy: fail-fast: false matrix: host: ${{ fromJson(needs.check-updates.outputs.hosts || '[]') }} steps: - name: Debug - show received values run: | echo "=== DEBUG: Values received from check-updates job ===" echo "needs.check-updates.outputs.changed = '${{ needs.check-updates.outputs.changed }}'" echo "needs.check-updates.outputs.hosts = '${{ needs.check-updates.outputs.hosts }}'" echo "matrix.host = '${{ matrix.host }}'" echo "=== END DEBUG ===" - name: Checkout repository uses: actions/checkout@v4 - name: Download updated flake.lock uses: forgejo/download-artifact@v4 with: name: flake-lock - name: Build ${{ matrix.host }} id: build run: | HOST="${{ matrix.host }}" echo "DEBUG: matrix.host='$HOST'" echo "DEBUG: raw matrix.host='${{ matrix.host }}'" if [ -z "$HOST" ]; then echo "Error: Host name is empty" echo "This usually means the 'hosts' output from check-updates job was not received properly" exit 1 fi echo "Building host: $HOST" nix build ".#nixosConfigurations.${HOST}.config.system.build.toplevel" --no-link 2>&1 | tee build-output.txt continue-on-error: true - name: Upload build log on failure if: failure() || steps.build.outcome == 'failure' uses: forgejo/upload-artifact@v4 with: name: build-failure-${{ matrix.host }} path: build-output.txt retention-days: 7 - name: Check build result run: | if [ "${{ steps.build.outcome }}" == "failure" ]; then echo "Build failed for ${{ matrix.host }}" exit 1 fi report: needs: [check-updates, build] if: always() && needs.check-updates.outputs.changed == 'true' runs-on: nix steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Configure Git run: | git config user.name "Flake Update Bot" git config user.email "bot@noreply.local" - name: Download updated flake.lock uses: forgejo/download-artifact@v4 with: name: flake-lock - name: Download failure artifacts if: needs.build.result == 'failure' uses: forgejo/download-artifact@v4 with: pattern: build-failure-* path: failures merge-multiple: false continue-on-error: true - name: Create branch and commit id: branch run: | BRANCH_NAME="auto-update/$(date +%Y-%m-%d)" git checkout -b "$BRANCH_NAME" git add flake.lock git commit -m "chore: update flake inputs $(date +%Y-%m-%d)" git push origin "$BRANCH_NAME" echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT - name: Create Pull Request if: needs.build.result == 'success' env: FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }} run: | HOSTS='${{ needs.check-updates.outputs.hosts }}' HOST_LIST=$(echo "$HOSTS" | jq -r '.[] | "- " + .' | tr '\n' '\n') curl -X POST \ -H "Authorization: token $FORGEJO_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "title": "chore: weekly flake update", "body": "Automated flake update from CI.\n\nThis PR updates all flake inputs and has been verified to build successfully.\n\n**Hosts built:**\n'"$(echo "$HOST_LIST" | sed 's/$/\\n/g' | tr -d '\n')"'\n\nGenerated on: '"$(date -Iseconds)"'", "head": "'"${{ steps.branch.outputs.branch_name }}"'", "base": "master" }' \ "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/pulls" - name: Create Issue on failure if: needs.build.result == 'failure' env: FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }} run: | # Collect failed hosts from artifact directories FAILED_HOSTS="" FAILURE_DETAILS="" if [ -d "failures" ]; then for dir in failures/build-failure-*; do if [ -d "$dir" ]; then HOST=$(basename "$dir" | sed 's/build-failure-//') FAILED_HOSTS="$FAILED_HOSTS\n- $HOST" if [ -f "$dir/build-output.txt" ]; then # Get last 50 lines of build output LOG_TAIL=$(tail -50 "$dir/build-output.txt" | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g') FAILURE_DETAILS="$FAILURE_DETAILS\n\n
\n$HOST build log (last 50 lines)\n\n\`\`\`\n$LOG_TAIL\n\`\`\`\n
" fi fi done fi # If no failure artifacts found, list from matrix if [ -z "$FAILED_HOSTS" ]; then FAILED_HOSTS="\n- (Unable to determine failed hosts - check workflow logs)" fi ISSUE_BODY="Weekly flake update failed to build some hosts.\n\n**Branch:** \`${{ steps.branch.outputs.branch_name }}\`\n**Run:** ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\n\n**Failed hosts:**$FAILED_HOSTS\n$FAILURE_DETAILS\n\nGenerated on: $(date -Iseconds)" curl -X POST \ -H "Authorization: token $FORGEJO_TOKEN" \ -H "Content-Type: application/json" \ -d "{ \"title\": \"Build failure: weekly flake update $(date +%Y-%m-%d)\", \"body\": \"$ISSUE_BODY\" }" \ "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues" - name: Cleanup if: always() run: | nix-collect-garbage -d rm -rf result .direnv failures